@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/lib/InstructionsPlaceholder.d.ts +6 -0
- package/lib/InstructionsPlaceholder.js +13 -0
- package/lib/InstructionsPlaceholder.js.map +1 -0
- package/lib/SettingsFormEditor.d.ts +2 -9
- package/lib/SettingsFormEditor.js +22 -14
- package/lib/SettingsFormEditor.js.map +1 -1
- package/lib/jsonsettingeditor.js +3 -7
- package/lib/jsonsettingeditor.js.map +1 -1
- package/lib/pluginlist.d.ts +4 -3
- package/lib/pluginlist.js +31 -23
- package/lib/pluginlist.js.map +1 -1
- package/lib/settingseditor.d.ts +4 -0
- package/lib/settingspanel.d.ts +5 -4
- package/lib/settingspanel.js +15 -38
- package/lib/settingspanel.js.map +1 -1
- package/lib/tokens.js +6 -2
- package/lib/tokens.js.map +1 -1
- package/package.json +30 -19
- package/src/InstructionsPlaceholder.tsx +30 -0
- package/src/SettingsFormEditor.tsx +62 -60
- package/src/jsonsettingeditor.tsx +3 -21
- package/src/pluginlist.tsx +46 -36
- package/src/settingseditor.tsx +7 -0
- package/src/settingspanel.tsx +26 -52
- package/src/tokens.ts +8 -2
- package/style/base.css +94 -144
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlab/settingeditor",
|
|
3
|
-
"version": "4.0.0-
|
|
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
|
-
"
|
|
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-
|
|
41
|
-
"@jupyterlab/apputils": "^4.0.0-
|
|
42
|
-
"@jupyterlab/codeeditor": "^4.0.0-
|
|
43
|
-
"@jupyterlab/inspector": "^4.0.0-
|
|
44
|
-
"@jupyterlab/rendermime": "^4.0.0-
|
|
45
|
-
"@jupyterlab/settingregistry": "^4.0.0-
|
|
46
|
-
"@jupyterlab/statedb": "^4.0.0-
|
|
47
|
-
"@jupyterlab/translation": "^4.0.0-
|
|
48
|
-
"@jupyterlab/ui-components": "^4.0.0-
|
|
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.
|
|
51
|
-
"@lumino/coreutils": "^2.
|
|
52
|
-
"@lumino/disposable": "^2.
|
|
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.
|
|
55
|
-
"@lumino/signaling": "^2.
|
|
56
|
-
"@lumino/widgets": "^2.
|
|
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
|
-
"
|
|
67
|
-
"
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
202
|
-
<div
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.props.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
<
|
|
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({
|
package/src/pluginlist.tsx
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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():
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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:
|
|
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
|
-
|
|
488
|
+
SettingsEditor.PluginSearchFilter
|
|
479
489
|
>(this);
|
|
480
490
|
private _allPlugins: ISettingRegistry.IPlugin[] = [];
|
|
481
491
|
private _settings: { [id: string]: Settings } = {};
|
package/src/settingseditor.tsx
CHANGED
|
@@ -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
|
*/
|
package/src/settingspanel.tsx
CHANGED
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
| Distributed under the terms of the Modified BSD License.
|
|
4
4
|
|----------------------------------------------------------------------------*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
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:
|
|
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 [
|
|
86
|
-
const [filterPlugin, setFilter] = useState<
|
|
87
|
-
(
|
|
88
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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}
|