@jupyterlab/settingeditor 4.0.0-beta.1 → 4.0.0-rc.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlab/settingeditor",
3
- "version": "4.0.0-beta.1",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "The JupyterLab default setting editor interface",
5
5
  "homepage": "https://github.com/jupyterlab/jupyterlab",
6
6
  "bugs": {
@@ -32,28 +32,33 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "build": "tsc -b",
35
- "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
35
+ "build:test": "tsc --build tsconfig.test.json",
36
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
36
37
  "docs": "typedoc src",
38
+ "test": "jest",
39
+ "test:cov": "jest --collect-coverage",
40
+ "test:debug": "node --inspect-brk ../../node_modules/.bin/jest --runInBand",
41
+ "test:debug:watch": "node --inspect-brk ../../node_modules/.bin/jest --runInBand --watch",
37
42
  "watch": "tsc -b --watch"
38
43
  },
39
44
  "dependencies": {
40
- "@jupyterlab/application": "^4.0.0-beta.1",
41
- "@jupyterlab/apputils": "^4.0.0-beta.1",
42
- "@jupyterlab/codeeditor": "^4.0.0-beta.1",
43
- "@jupyterlab/inspector": "^4.0.0-beta.1",
44
- "@jupyterlab/rendermime": "^4.0.0-beta.1",
45
- "@jupyterlab/settingregistry": "^4.0.0-beta.1",
46
- "@jupyterlab/statedb": "^4.0.0-beta.1",
47
- "@jupyterlab/translation": "^4.0.0-beta.1",
48
- "@jupyterlab/ui-components": "^4.0.0-beta.1",
45
+ "@jupyterlab/application": "^4.0.0-rc.0",
46
+ "@jupyterlab/apputils": "^4.0.0-rc.0",
47
+ "@jupyterlab/codeeditor": "^4.0.0-rc.0",
48
+ "@jupyterlab/inspector": "^4.0.0-rc.0",
49
+ "@jupyterlab/rendermime": "^4.0.0-rc.0",
50
+ "@jupyterlab/settingregistry": "^4.0.0-rc.0",
51
+ "@jupyterlab/statedb": "^4.0.0-rc.0",
52
+ "@jupyterlab/translation": "^4.0.0-rc.0",
53
+ "@jupyterlab/ui-components": "^4.0.0-rc.0",
49
54
  "@lumino/algorithm": "^2.0.0",
50
- "@lumino/commands": "^2.0.1",
51
- "@lumino/coreutils": "^2.0.0",
52
- "@lumino/disposable": "^2.0.0",
55
+ "@lumino/commands": "^2.1.1",
56
+ "@lumino/coreutils": "^2.1.1",
57
+ "@lumino/disposable": "^2.1.1",
53
58
  "@lumino/messaging": "^2.0.0",
54
- "@lumino/polling": "^2.0.0",
55
- "@lumino/signaling": "^2.0.0",
56
- "@lumino/widgets": "^2.0.1",
59
+ "@lumino/polling": "^2.1.1",
60
+ "@lumino/signaling": "^2.1.1",
61
+ "@lumino/widgets": "^2.1.1",
57
62
  "@rjsf/core": "^5.1.0",
58
63
  "@rjsf/utils": "^5.1.0",
59
64
  "@rjsf/validator-ajv8": "^5.1.0",
@@ -61,10 +66,16 @@
61
66
  "react": "^18.2.0"
62
67
  },
63
68
  "devDependencies": {
69
+ "@jupyterlab/testing": "^4.0.0-rc.0",
70
+ "@types/jest": "^29.2.0",
64
71
  "@types/react": "^18.0.26",
72
+ "@types/react-test-renderer": "^18.0.0",
73
+ "jest": "^29.2.0",
74
+ "react-test-renderer": "^18.2.0",
65
75
  "rimraf": "~3.0.0",
66
- "typedoc": "~0.23.25",
67
- "typescript": "~5.0.2"
76
+ "ts-jest": "^29.1.0",
77
+ "typedoc": "~0.24.1",
78
+ "typescript": "~5.0.4"
68
79
  },
69
80
  "publishConfig": {
70
81
  "access": "public"
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React from 'react';
7
+
8
+ import { ITranslator } from '@jupyterlab/translation';
9
+
10
+ type ISettingsEditorPlaceholderProps = {
11
+ translator: ITranslator;
12
+ };
13
+
14
+ export const SettingsEditorPlaceholder = ({
15
+ translator
16
+ }: ISettingsEditorPlaceholderProps) => {
17
+ const trans = translator.load('jupyterlab');
18
+ return (
19
+ <div className="jp-SettingsEditor-placeholder">
20
+ <div className="jp-SettingsEditor-placeholderContent">
21
+ <h3>{trans.__('No Plugin Selected')}</h3>
22
+ <p>
23
+ {trans.__(
24
+ 'Select a plugin from the list to view and edit its preferences.'
25
+ )}
26
+ </p>
27
+ </div>
28
+ </div>
29
+ );
30
+ };
@@ -3,21 +3,24 @@
3
3
  | Distributed under the terms of the Modified BSD License.
4
4
  |----------------------------------------------------------------------------*/
5
5
 
6
+ import React from 'react';
7
+
6
8
  import { showErrorMessage } from '@jupyterlab/apputils';
7
9
  import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
8
10
  import { ITranslator } from '@jupyterlab/translation';
11
+ import { FormComponent } from '@jupyterlab/ui-components';
9
12
  import {
10
- caretDownIcon,
11
- caretRightIcon,
12
- FormComponent
13
- } from '@jupyterlab/ui-components';
14
- import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils';
13
+ JSONExt,
14
+ PartialJSONObject,
15
+ ReadonlyJSONObject,
16
+ ReadonlyPartialJSONObject
17
+ } from '@lumino/coreutils';
15
18
  import { Debouncer } from '@lumino/polling';
16
19
  import { IChangeEvent } from '@rjsf/core';
17
20
  import validatorAjv8 from '@rjsf/validator-ajv8';
18
21
  import { Field, UiSchema } from '@rjsf/utils';
19
22
  import { JSONSchema7 } from 'json-schema';
20
- import React from 'react';
23
+ import { Button } from '@jupyterlab/ui-components';
21
24
 
22
25
  /**
23
26
  * Indentation to use when saving the settings as JSON document.
@@ -43,16 +46,6 @@ export namespace SettingsFormEditor {
43
46
  */
44
47
  renderers: { [id: string]: { [property: string]: Field } };
45
48
 
46
- /**
47
- * Whether the form is collapsed or not.
48
- */
49
- isCollapsed: boolean;
50
-
51
- /**
52
- * Callback with the collapse state value.
53
- */
54
- onCollapseChange: (v: boolean) => void;
55
-
56
49
  /**
57
50
  * Translator object
58
51
  */
@@ -117,7 +110,7 @@ export class SettingsFormEditor extends React.Component<
117
110
  constructor(props: SettingsFormEditor.IProps) {
118
111
  super(props);
119
112
  const { settings } = props;
120
- this._formData = settings.composite;
113
+ this._formData = settings.composite as ReadonlyJSONObject;
121
114
  this.state = {
122
115
  isModified: settings.isModified,
123
116
  uiSchema: {},
@@ -161,6 +154,7 @@ export class SettingsFormEditor extends React.Component<
161
154
  // Prevent unnecessary save when opening settings that haven't been modified.
162
155
  if (
163
156
  !this.props.settings.isModified &&
157
+ this._formData &&
164
158
  this.props.settings.isDefault(this._formData)
165
159
  ) {
166
160
  this.props.updateDirtyState(false);
@@ -189,61 +183,52 @@ export class SettingsFormEditor extends React.Component<
189
183
  for (const field in this.props.settings.user) {
190
184
  await this.props.settings.remove(field);
191
185
  }
192
- this._formData = this.props.settings.composite;
186
+ this._formData = this.props.settings.composite as ReadonlyJSONObject;
193
187
  this.setState({ isModified: false });
194
188
  };
195
189
 
196
190
  render(): JSX.Element {
197
191
  const trans = this.props.translator.load('jupyterlab');
198
- const icon = this.props.isCollapsed ? caretRightIcon : caretDownIcon;
199
192
 
200
193
  return (
201
- <div>
202
- <div
203
- className="jp-SettingsHeader"
204
- onClick={() => {
205
- this.props.onCollapseChange(!this.props.isCollapsed);
206
- this.props.onSelect(this.props.settings.id);
207
- }}
208
- >
209
- <header className="jp-SettingsTitle">
210
- <icon.react
211
- tag="span"
212
- elementPosition="center"
213
- className="jp-SettingsTitle-caret"
214
- />
215
- <h2>{this.props.settings.schema.title}</h2>
216
- <div className="jp-SettingsHeader-description">
217
- {this.props.settings.schema.description}
218
- </div>
219
- </header>
220
- {this.state.isModified && (
221
- <button className="jp-RestoreButton" onClick={this.reset}>
222
- {trans.__('Restore to Defaults')}
223
- </button>
224
- )}
194
+ <>
195
+ <div className="jp-SettingsHeader">
196
+ <h2
197
+ className="jp-SettingsHeader-title"
198
+ title={this.props.settings.schema.description}
199
+ >
200
+ {this.props.settings.schema.title}
201
+ </h2>
202
+ <div className="jp-SettingsHeader-buttonbar">
203
+ {this.state.isModified && (
204
+ <Button className="jp-RestoreButton" onClick={this.reset}>
205
+ {trans.__('Restore to Defaults')}
206
+ </Button>
207
+ )}
208
+ </div>
209
+ <div className="jp-SettingsHeader-description">
210
+ {this.props.settings.schema.description}
211
+ </div>
225
212
  </div>
226
- {!this.props.isCollapsed && (
227
- <FormComponent
228
- validator={validatorAjv8}
229
- schema={this.state.filteredSchema as JSONSchema7}
230
- formData={this._formData}
231
- uiSchema={this.state.uiSchema}
232
- fields={this.props.renderers[this.props.settings.id]}
233
- formContext={this.state.formContext}
234
- liveValidate
235
- idPrefix={`jp-SettingsEditor-${this.props.settings.id}`}
236
- onChange={this._onChange}
237
- translator={this.props.translator}
238
- />
239
- )}
240
- </div>
213
+ <FormComponent
214
+ validator={validatorAjv8}
215
+ schema={this.state.filteredSchema as JSONSchema7}
216
+ formData={this._getFilteredFormData(this.state.filteredSchema)}
217
+ uiSchema={this.state.uiSchema}
218
+ fields={this.props.renderers[this.props.settings.id]}
219
+ formContext={this.state.formContext}
220
+ liveValidate
221
+ idPrefix={`jp-SettingsEditor-${this.props.settings.id}`}
222
+ onChange={this._onChange}
223
+ translator={this.props.translator}
224
+ />
225
+ </>
241
226
  );
242
227
  }
243
228
 
244
229
  private _onChange = (e: IChangeEvent<ReadonlyPartialJSONObject>): void => {
245
230
  this.props.hasError(e.errors.length !== 0);
246
- this._formData = e.formData;
231
+ this._formData = e.formData as ReadonlyJSONObject;
247
232
  if (e.errors.length === 0) {
248
233
  this.props.updateDirtyState(true);
249
234
  void this._debouncer.invoke();
@@ -301,6 +286,23 @@ export class SettingsFormEditor extends React.Component<
301
286
  }
302
287
  }
303
288
 
289
+ private _getFilteredFormData(
290
+ filteredSchema?: ISettingRegistry.ISchema
291
+ ): ReadonlyJSONObject {
292
+ if (!filteredSchema?.properties) {
293
+ return this._formData;
294
+ }
295
+ const filteredFormData = JSONExt.deepCopy(
296
+ this._formData as PartialJSONObject
297
+ );
298
+ for (const field in filteredFormData) {
299
+ if (!filteredSchema.properties[field]) {
300
+ delete filteredFormData[field];
301
+ }
302
+ }
303
+ return filteredFormData as ReadonlyJSONObject;
304
+ }
305
+
304
306
  private _debouncer: Debouncer<void, any>;
305
- private _formData: any;
307
+ private _formData: ReadonlyJSONObject;
306
308
  }
@@ -8,13 +8,14 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
8
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
9
9
  import { IStateDB } from '@jupyterlab/statedb';
10
10
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
11
- import { jupyterIcon, ReactWidget } from '@jupyterlab/ui-components';
11
+ import { ReactWidget } from '@jupyterlab/ui-components';
12
12
  import { CommandRegistry } from '@lumino/commands';
13
13
  import { JSONExt, JSONObject, JSONValue } from '@lumino/coreutils';
14
14
  import { Message } from '@lumino/messaging';
15
15
  import { ISignal } from '@lumino/signaling';
16
16
  import { SplitPanel, Widget } from '@lumino/widgets';
17
17
  import * as React from 'react';
18
+ import { SettingsEditorPlaceholder } from './InstructionsPlaceholder';
18
19
  import { PluginEditor } from './plugineditor';
19
20
  import { PluginList } from './pluginlist';
20
21
 
@@ -44,7 +45,6 @@ export class JsonSettingEditor extends SplitPanel {
44
45
  spacing: 1
45
46
  });
46
47
  this.translator = options.translator || nullTranslator;
47
- const trans = this.translator.load('jupyterlab');
48
48
  this.addClass('jp-SettingEditor');
49
49
  this.key = options.key;
50
50
  this.state = options.state;
@@ -52,25 +52,7 @@ export class JsonSettingEditor extends SplitPanel {
52
52
  const { commands, editorFactory, rendermime } = options;
53
53
  const registry = (this.registry = options.registry);
54
54
  const instructions = (this._instructions = ReactWidget.create(
55
- <React.Fragment>
56
- <h2>
57
- <jupyterIcon.react
58
- className="jp-SettingEditorInstructions-icon"
59
- tag="span"
60
- elementPosition="center"
61
- height="auto"
62
- width="60px"
63
- />
64
- <span className="jp-SettingEditorInstructions-title">
65
- {trans.__('Settings')}
66
- </span>
67
- </h2>
68
- <span className="jp-SettingEditorInstructions-text">
69
- {trans.__(
70
- 'Select a plugin from the list to view and edit its preferences.'
71
- )}
72
- </span>
73
- </React.Fragment>
55
+ <SettingsEditorPlaceholder translator={this.translator} />
74
56
  ));
75
57
  instructions.addClass('jp-SettingEditorInstructions');
76
58
  const editor = (this._editor = new PluginEditor({
@@ -3,6 +3,8 @@
3
3
  | Distributed under the terms of the Modified BSD License.
4
4
  |----------------------------------------------------------------------------*/
5
5
 
6
+ import React from 'react';
7
+
6
8
  import { ReactWidget } from '@jupyterlab/apputils';
7
9
  import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
8
10
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
@@ -18,7 +20,8 @@ import { StringExt } from '@lumino/algorithm';
18
20
  import { PartialJSONObject } from '@lumino/coreutils';
19
21
  import { Message } from '@lumino/messaging';
20
22
  import { ISignal, Signal } from '@lumino/signaling';
21
- import React from 'react';
23
+
24
+ import type { SettingsEditor } from './settingseditor';
22
25
 
23
26
  /**
24
27
  * The JupyterLab plugin schema key for the setting editor
@@ -56,7 +59,9 @@ export class PluginList extends ReactWidget {
56
59
  }, this);
57
60
  this.mapPlugins = this.mapPlugins.bind(this);
58
61
  this.setFilter = this.setFilter.bind(this);
59
- this.setFilter(updateFilterFunction(options.query ?? '', false, false));
62
+ this.setFilter(
63
+ options.query ? updateFilterFunction(options.query, false, false) : null
64
+ );
60
65
  this.setError = this.setError.bind(this);
61
66
  this._evtMousedown = this._evtMousedown.bind(this);
62
67
  this._query = options.query;
@@ -91,7 +96,6 @@ export class PluginList extends ReactWidget {
91
96
  void loadSettings();
92
97
 
93
98
  this._errors = {};
94
- this.selection = this._allPlugins[0].id;
95
99
  }
96
100
 
97
101
  /**
@@ -122,7 +126,7 @@ export class PluginList extends ReactWidget {
122
126
  return false;
123
127
  }
124
128
 
125
- get filter(): (item: ISettingRegistry.IPlugin) => string[] | null {
129
+ get filter(): SettingsEditor.PluginSearchFilter {
126
130
  return this._filter;
127
131
  }
128
132
 
@@ -140,10 +144,7 @@ export class PluginList extends ReactWidget {
140
144
  /**
141
145
  * Signal that fires when search filter is updated so that settings panel can filter results.
142
146
  */
143
- get updateFilterSignal(): ISignal<
144
- this,
145
- (plugin: ISettingRegistry.IPlugin) => string[] | null
146
- > {
147
+ get updateFilterSignal(): ISignal<this, SettingsEditor.PluginSearchFilter> {
147
148
  return this._updateFilterSignal;
148
149
  }
149
150
 
@@ -321,20 +322,24 @@ export class PluginList extends ReactWidget {
321
322
  * @param filter Filter function passed by search bar based on search value.
322
323
  */
323
324
  setFilter(
324
- filter: (item: string) => Partial<IScore> | null,
325
+ filter: ((item: string) => Partial<IScore> | null) | null,
325
326
  query?: string
326
327
  ): void {
327
- this._filter = (plugin: ISettingRegistry.IPlugin): string[] | null => {
328
- if (filter(plugin.schema.title ?? '')) {
329
- return null;
330
- }
331
- const filtered = this.getFilterString(
332
- filter,
333
- plugin.schema ?? {},
334
- plugin.schema.definitions
335
- );
336
- return filtered;
337
- };
328
+ if (filter) {
329
+ this._filter = (plugin: ISettingRegistry.IPlugin): string[] | null => {
330
+ if (!filter || filter(plugin.schema.title ?? '')) {
331
+ return null;
332
+ }
333
+ const filtered = this.getFilterString(
334
+ filter,
335
+ plugin.schema ?? {},
336
+ plugin.schema.definitions
337
+ );
338
+ return filtered;
339
+ };
340
+ } else {
341
+ this._filter = null;
342
+ }
338
343
  this._query = query;
339
344
  this._updateFilterSignal.emit(this._filter);
340
345
  this.update();
@@ -373,20 +378,22 @@ export class PluginList extends ReactWidget {
373
378
  const icon = this.getHint(ICON_KEY, this.registry, plugin);
374
379
  const iconClass = this.getHint(ICON_CLASS_KEY, this.registry, plugin);
375
380
  const iconTitle = this.getHint(ICON_LABEL_KEY, this.registry, plugin);
376
- const filteredProperties = this._filter(plugin)?.map(fieldValue => {
377
- const highlightedIndices = StringExt.matchSumOfSquares(
378
- fieldValue.toLocaleLowerCase(),
379
- this._query?.toLocaleLowerCase() ?? ''
380
- );
381
- const highlighted = StringExt.highlight(
382
- fieldValue,
383
- highlightedIndices?.indices ?? [],
384
- chunk => {
385
- return <mark>{chunk}</mark>;
386
- }
387
- );
388
- return <li key={`${id}-${fieldValue}`}> {highlighted} </li>;
389
- });
381
+ const filteredProperties = this._filter
382
+ ? this._filter(plugin)?.map(fieldValue => {
383
+ const highlightedIndices = StringExt.matchSumOfSquares(
384
+ fieldValue.toLocaleLowerCase(),
385
+ this._query?.toLocaleLowerCase() ?? ''
386
+ );
387
+ const highlighted = StringExt.highlight(
388
+ fieldValue,
389
+ highlightedIndices?.indices ?? [],
390
+ chunk => {
391
+ return <mark>{chunk}</mark>;
392
+ }
393
+ );
394
+ return <li key={`${id}-${fieldValue}`}> {highlighted} </li>;
395
+ })
396
+ : undefined;
390
397
 
391
398
  return (
392
399
  <div
@@ -422,6 +429,9 @@ export class PluginList extends ReactWidget {
422
429
  const trans = this.translator.load('jupyterlab');
423
430
  // Filter all plugins based on search value before displaying list.
424
431
  const allPlugins = this._allPlugins.filter(plugin => {
432
+ if (!this._filter) {
433
+ return false;
434
+ }
425
435
  const filtered = this._filter(plugin);
426
436
  return filtered === null || filtered.length > 0;
427
437
  });
@@ -470,12 +480,12 @@ export class PluginList extends ReactWidget {
470
480
  protected translator: ITranslator;
471
481
  private _changed = new Signal<this, void>(this);
472
482
  private _errors: { [id: string]: boolean };
473
- private _filter: (item: ISettingRegistry.IPlugin) => string[] | null;
483
+ private _filter: SettingsEditor.PluginSearchFilter;
474
484
  private _query: string | undefined;
475
485
  private _handleSelectSignal = new Signal<this, string>(this);
476
486
  private _updateFilterSignal = new Signal<
477
487
  this,
478
- (plugin: ISettingRegistry.IPlugin) => string[] | null
488
+ SettingsEditor.PluginSearchFilter
479
489
  >(this);
480
490
  private _allPlugins: ISettingRegistry.IPlugin[] = [];
481
491
  private _settings: { [id: string]: Settings } = {};
@@ -161,6 +161,13 @@ export namespace SettingsEditor {
161
161
  */
162
162
  export type SaveState = 'started' | 'failed' | 'completed';
163
163
 
164
+ /**
165
+ *
166
+ */
167
+ export type PluginSearchFilter =
168
+ | ((plugin: ISettingRegistry.IPlugin) => string[] | null)
169
+ | null;
170
+
164
171
  /**
165
172
  * Settings editor options
166
173
  */
@@ -3,15 +3,18 @@
3
3
  | Distributed under the terms of the Modified BSD License.
4
4
  |----------------------------------------------------------------------------*/
5
5
 
6
- import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ import { Settings } from '@jupyterlab/settingregistry';
7
9
  import { ITranslator } from '@jupyterlab/translation';
8
10
  import { IFormRendererRegistry } from '@jupyterlab/ui-components';
9
11
  import { ISignal } from '@lumino/signaling';
10
- import type { Field } from '@rjsf/utils';
11
- import React, { useEffect, useState } from 'react';
12
12
  import { PluginList } from './pluginlist';
13
13
  import { SettingsFormEditor } from './SettingsFormEditor';
14
+ import { SettingsEditorPlaceholder } from './InstructionsPlaceholder';
14
15
 
16
+ import type { Field } from '@rjsf/utils';
17
+ import type { SettingsEditor } from './settingseditor';
15
18
  export interface ISettingsPanelProps {
16
19
  /**
17
20
  * List of Settings objects that provide schema and values
@@ -55,16 +58,13 @@ export interface ISettingsPanelProps {
55
58
  /**
56
59
  * Signal that sends updated filter when search value changes.
57
60
  */
58
- updateFilterSignal: ISignal<
59
- PluginList,
60
- (plugin: ISettingRegistry.IPlugin) => string[] | null
61
- >;
61
+ updateFilterSignal: ISignal<PluginList, SettingsEditor.PluginSearchFilter>;
62
62
 
63
63
  /**
64
64
  * If the settings editor is created with an initial search query, an initial
65
65
  * filter function is passed to the settings panel.
66
66
  */
67
- initialFilter: (item: ISettingRegistry.IPlugin) => string[] | null;
67
+ initialFilter: SettingsEditor.PluginSearchFilter;
68
68
  }
69
69
 
70
70
  /**
@@ -82,18 +82,10 @@ export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
82
82
  translator,
83
83
  initialFilter
84
84
  }: ISettingsPanelProps): JSX.Element => {
85
- const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null);
86
- const [filterPlugin, setFilter] = useState<
87
- (plugin: ISettingRegistry.IPlugin) => string[] | null
88
- >(() => initialFilter);
89
-
90
- // Refs used to keep track of "selected" plugin based on scroll location
91
- const editorRefs: {
92
- [pluginId: string]: React.RefObject<HTMLDivElement>;
93
- } = {};
94
- for (const setting of settings) {
95
- editorRefs[setting.id] = React.useRef(null);
96
- }
85
+ const [activePluginId, setActivePluginId] = useState<string | null>(null);
86
+ const [filterPlugin, setFilter] = useState<SettingsEditor.PluginSearchFilter>(
87
+ initialFilter ? () => initialFilter : null
88
+ );
97
89
  const wrapperRef: React.RefObject<HTMLDivElement> = React.useRef(null);
98
90
  const editorDirtyStates: React.RefObject<{
99
91
  [id: string]: boolean;
@@ -102,34 +94,16 @@ export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
102
94
  useEffect(() => {
103
95
  const onFilterUpdate = (
104
96
  list: PluginList,
105
- newFilter: (plugin: ISettingRegistry.IPlugin) => string[] | null
97
+ newFilter: SettingsEditor.PluginSearchFilter
106
98
  ) => {
107
- setFilter(() => newFilter);
108
- for (const pluginSettings of settings) {
109
- const filtered = newFilter(pluginSettings.plugin);
110
- if (filtered === null || filtered.length > 0) {
111
- setExpandedPlugin(pluginSettings.id);
112
- break;
113
- }
114
- }
99
+ newFilter ? setFilter(() => newFilter) : setFilter(null);
115
100
  };
116
101
 
117
- // Set first visible plugin as expanded plugin on initial load.
118
- for (const pluginSettings of settings) {
119
- const filtered = filterPlugin(pluginSettings.plugin);
120
- if (filtered === null || filtered.length > 0) {
121
- setExpandedPlugin(pluginSettings.id);
122
- break;
123
- }
124
- }
125
-
126
102
  // When filter updates, only show plugins that match search.
127
103
  updateFilterSignal.connect(onFilterUpdate);
128
104
 
129
105
  const onSelectChange = (list: PluginList, pluginId: string) => {
130
- setExpandedPlugin(expandedPlugin !== pluginId ? pluginId : null);
131
- // Scroll to the plugin when a selection is made in the left panel.
132
- editorRefs[pluginId]?.current?.scrollIntoView(true);
106
+ setActivePluginId(pluginId);
133
107
  };
134
108
  handleSelectSignal?.connect?.(onSelectChange);
135
109
 
@@ -174,30 +148,30 @@ export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
174
148
  [editorRegistry]
175
149
  );
176
150
 
151
+ if (!activePluginId && !filterPlugin) {
152
+ return <SettingsEditorPlaceholder translator={translator} />;
153
+ }
154
+
177
155
  return (
178
156
  <div className="jp-SettingsPanel" ref={wrapperRef}>
179
157
  {settings.map(pluginSettings => {
180
158
  // Pass filtered results to SettingsFormEditor to only display filtered fields.
181
- const filtered = filterPlugin(pluginSettings.plugin);
159
+ const filtered = filterPlugin
160
+ ? filterPlugin(pluginSettings.plugin)
161
+ : null;
182
162
  // If filtered results are an array, only show if the array is non-empty.
183
- if (filtered !== null && filtered.length === 0) {
163
+ if (
164
+ (activePluginId && activePluginId !== pluginSettings.id) ||
165
+ (filtered !== null && filtered.length === 0)
166
+ ) {
184
167
  return undefined;
185
168
  }
186
169
  return (
187
170
  <div
188
- ref={editorRefs[pluginSettings.id]}
189
171
  className="jp-SettingsForm"
190
172
  key={`${pluginSettings.id}SettingsEditor`}
191
173
  >
192
174
  <SettingsFormEditor
193
- isCollapsed={pluginSettings.id !== expandedPlugin}
194
- onCollapseChange={(willCollapse: boolean) => {
195
- if (!willCollapse) {
196
- setExpandedPlugin(pluginSettings.id);
197
- } else if (pluginSettings.id === expandedPlugin) {
198
- setExpandedPlugin(null);
199
- }
200
- }}
201
175
  filteredValues={filtered}
202
176
  settings={pluginSettings}
203
177
  renderers={renderers}