@jupyterlab/settingeditor 4.0.0-alpha.2 → 4.0.0-alpha.20
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/SettingsFormEditor.d.ts +101 -0
- package/lib/SettingsFormEditor.js +151 -0
- package/lib/SettingsFormEditor.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/inspector.js +24 -30
- package/lib/inspector.js.map +1 -1
- package/lib/{settingeditor.d.ts → jsonsettingeditor.d.ts} +5 -6
- package/lib/{settingeditor.js → jsonsettingeditor.js} +25 -43
- package/lib/jsonsettingeditor.js.map +1 -0
- package/lib/plugineditor.d.ts +3 -3
- package/lib/plugineditor.js +2 -6
- package/lib/plugineditor.js.map +1 -1
- package/lib/pluginlist.d.ts +58 -22
- package/lib/pluginlist.js +246 -109
- package/lib/pluginlist.js.map +1 -1
- package/lib/raweditor.d.ts +2 -6
- package/lib/raweditor.js +22 -33
- package/lib/raweditor.js.map +1 -1
- package/lib/settingseditor.d.ts +81 -0
- package/lib/settingseditor.js +121 -0
- package/lib/settingseditor.js.map +1 -0
- package/lib/settingspanel.d.ts +54 -0
- package/lib/settingspanel.js +102 -0
- package/lib/settingspanel.js.map +1 -0
- package/lib/tokens.d.ts +12 -2
- package/lib/tokens.js +4 -1
- package/lib/tokens.js.map +1 -1
- package/package.json +28 -21
- package/src/SettingsFormEditor.tsx +306 -0
- package/src/index.ts +10 -0
- package/src/inspector.ts +144 -0
- package/src/jsonsettingeditor.tsx +482 -0
- package/src/plugineditor.ts +257 -0
- package/src/pluginlist.tsx +538 -0
- package/src/raweditor.ts +422 -0
- package/src/settingseditor.tsx +210 -0
- package/src/settingspanel.tsx +218 -0
- package/src/tokens.ts +33 -0
- package/style/base.css +213 -89
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/lib/settingeditor.js.map +0 -1
- package/lib/splitpanel.d.ts +0 -13
- package/lib/splitpanel.js +0 -26
- package/lib/splitpanel.js.map +0 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import { ReactWidget } from '@jupyterlab/apputils';
|
|
7
|
+
import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
|
|
8
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
9
|
+
import {
|
|
10
|
+
classes,
|
|
11
|
+
FilterBox,
|
|
12
|
+
IScore,
|
|
13
|
+
LabIcon,
|
|
14
|
+
settingsIcon,
|
|
15
|
+
updateFilterFunction
|
|
16
|
+
} from '@jupyterlab/ui-components';
|
|
17
|
+
import { StringExt } from '@lumino/algorithm';
|
|
18
|
+
import { PartialJSONObject } from '@lumino/coreutils';
|
|
19
|
+
import { Message } from '@lumino/messaging';
|
|
20
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
21
|
+
import React from 'react';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The JupyterLab plugin schema key for the setting editor
|
|
25
|
+
* icon class of a plugin.
|
|
26
|
+
*/
|
|
27
|
+
const ICON_KEY = 'jupyter.lab.setting-icon';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The JupyterLab plugin schema key for the setting editor
|
|
31
|
+
* icon class of a plugin.
|
|
32
|
+
*/
|
|
33
|
+
const ICON_CLASS_KEY = 'jupyter.lab.setting-icon-class';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The JupyterLab plugin schema key for the setting editor
|
|
37
|
+
* icon label of a plugin.
|
|
38
|
+
*/
|
|
39
|
+
const ICON_LABEL_KEY = 'jupyter.lab.setting-icon-label';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A list of plugins with editable settings.
|
|
43
|
+
*/
|
|
44
|
+
export class PluginList extends ReactWidget {
|
|
45
|
+
/**
|
|
46
|
+
* Create a new plugin list.
|
|
47
|
+
*/
|
|
48
|
+
constructor(options: PluginList.IOptions) {
|
|
49
|
+
super();
|
|
50
|
+
this.registry = options.registry;
|
|
51
|
+
this.translator = options.translator || nullTranslator;
|
|
52
|
+
this.addClass('jp-PluginList');
|
|
53
|
+
this._confirm = options.confirm;
|
|
54
|
+
this.registry.pluginChanged.connect(() => {
|
|
55
|
+
this.update();
|
|
56
|
+
}, this);
|
|
57
|
+
this.mapPlugins = this.mapPlugins.bind(this);
|
|
58
|
+
this.setFilter = this.setFilter.bind(this);
|
|
59
|
+
this.setFilter(updateFilterFunction(options.query ?? '', false, false));
|
|
60
|
+
this.setError = this.setError.bind(this);
|
|
61
|
+
this._evtMousedown = this._evtMousedown.bind(this);
|
|
62
|
+
this._query = options.query;
|
|
63
|
+
|
|
64
|
+
this._allPlugins = PluginList.sortPlugins(this.registry).filter(plugin => {
|
|
65
|
+
const { schema } = plugin;
|
|
66
|
+
const deprecated = schema['jupyter.lab.setting-deprecated'] === true;
|
|
67
|
+
const editable = Object.keys(schema.properties || {}).length > 0;
|
|
68
|
+
const extensible = schema.additionalProperties !== false;
|
|
69
|
+
// Filters out a couple of plugins that take too long to load in the new settings editor.
|
|
70
|
+
const correctEditor =
|
|
71
|
+
// If this is the json settings editor, anything is fine
|
|
72
|
+
this._confirm ||
|
|
73
|
+
// If this is the new settings editor, remove context menu / main menu settings.
|
|
74
|
+
(!this._confirm && !(options.toSkip ?? []).includes(plugin.id));
|
|
75
|
+
|
|
76
|
+
return !deprecated && correctEditor && (editable || extensible);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loads all settings and stores them for easy access when displaying search results.
|
|
81
|
+
*/
|
|
82
|
+
const loadSettings = async () => {
|
|
83
|
+
for (const plugin of this._allPlugins) {
|
|
84
|
+
const pluginSettings = (await this.registry.load(
|
|
85
|
+
plugin.id
|
|
86
|
+
)) as Settings;
|
|
87
|
+
this._settings[plugin.id] = pluginSettings;
|
|
88
|
+
}
|
|
89
|
+
this.update();
|
|
90
|
+
};
|
|
91
|
+
void loadSettings();
|
|
92
|
+
|
|
93
|
+
this._errors = {};
|
|
94
|
+
this.selection = this._allPlugins[0].id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The setting registry.
|
|
99
|
+
*/
|
|
100
|
+
readonly registry: ISettingRegistry;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A signal emitted when a list user interaction happens.
|
|
104
|
+
*/
|
|
105
|
+
get changed(): ISignal<this, void> {
|
|
106
|
+
return this._changed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The selection value of the plugin list.
|
|
111
|
+
*/
|
|
112
|
+
get scrollTop(): number | undefined {
|
|
113
|
+
return this.node.querySelector('ul')?.scrollTop;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get hasErrors(): boolean {
|
|
117
|
+
for (const id in this._errors) {
|
|
118
|
+
if (this._errors[id]) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get filter(): (item: ISettingRegistry.IPlugin) => string[] | null {
|
|
126
|
+
return this._filter;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The selection value of the plugin list.
|
|
131
|
+
*/
|
|
132
|
+
get selection(): string {
|
|
133
|
+
return this._selection;
|
|
134
|
+
}
|
|
135
|
+
set selection(selection: string) {
|
|
136
|
+
this._selection = selection;
|
|
137
|
+
this.update();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Signal that fires when search filter is updated so that settings panel can filter results.
|
|
142
|
+
*/
|
|
143
|
+
get updateFilterSignal(): ISignal<
|
|
144
|
+
this,
|
|
145
|
+
(plugin: ISettingRegistry.IPlugin) => string[] | null
|
|
146
|
+
> {
|
|
147
|
+
return this._updateFilterSignal;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get handleSelectSignal(): ISignal<this, string> {
|
|
151
|
+
return this._handleSelectSignal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle `'update-request'` messages.
|
|
156
|
+
*/
|
|
157
|
+
protected onUpdateRequest(msg: Message): void {
|
|
158
|
+
const ul = this.node.querySelector('ul');
|
|
159
|
+
if (ul && this._scrollTop !== undefined) {
|
|
160
|
+
ul.scrollTop = this._scrollTop;
|
|
161
|
+
}
|
|
162
|
+
super.onUpdateRequest(msg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle the `'mousedown'` event for the plugin list.
|
|
167
|
+
*
|
|
168
|
+
* @param event - The DOM event sent to the widget
|
|
169
|
+
*/
|
|
170
|
+
private _evtMousedown(event: React.MouseEvent<HTMLDivElement>): void {
|
|
171
|
+
const target = event.currentTarget;
|
|
172
|
+
const id = target.getAttribute('data-id');
|
|
173
|
+
|
|
174
|
+
if (!id) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (this._confirm) {
|
|
179
|
+
this._confirm(id)
|
|
180
|
+
.then(() => {
|
|
181
|
+
this.selection = id!;
|
|
182
|
+
this._changed.emit(undefined);
|
|
183
|
+
this.update();
|
|
184
|
+
})
|
|
185
|
+
.catch(() => {
|
|
186
|
+
/* no op */
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
this._scrollTop = this.scrollTop;
|
|
190
|
+
this._selection = id!;
|
|
191
|
+
this._handleSelectSignal.emit(id!);
|
|
192
|
+
this._changed.emit(undefined);
|
|
193
|
+
this.update();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check the plugin for a rendering hint's value.
|
|
199
|
+
*
|
|
200
|
+
* #### Notes
|
|
201
|
+
* The order of priority for overridden hints is as follows, from most
|
|
202
|
+
* important to least:
|
|
203
|
+
* 1. Data set by the end user in a settings file.
|
|
204
|
+
* 2. Data set by the plugin author as a schema default.
|
|
205
|
+
* 3. Data set by the plugin author as a top-level key of the schema.
|
|
206
|
+
*/
|
|
207
|
+
getHint(
|
|
208
|
+
key: string,
|
|
209
|
+
registry: ISettingRegistry,
|
|
210
|
+
plugin: ISettingRegistry.IPlugin
|
|
211
|
+
): string {
|
|
212
|
+
// First, give priority to checking if the hint exists in the user data.
|
|
213
|
+
let hint = plugin.data.user[key];
|
|
214
|
+
|
|
215
|
+
// Second, check to see if the hint exists in composite data, which folds
|
|
216
|
+
// in default values from the schema.
|
|
217
|
+
if (!hint) {
|
|
218
|
+
hint = plugin.data.composite[key];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Third, check to see if the plugin schema has defined the hint.
|
|
222
|
+
if (!hint) {
|
|
223
|
+
hint = plugin.schema[key];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Finally, use the defaults from the registry schema.
|
|
227
|
+
if (!hint) {
|
|
228
|
+
const { properties } = registry.schema;
|
|
229
|
+
|
|
230
|
+
hint = properties && properties[key] && properties[key].default;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return typeof hint === 'string' ? hint : '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Function to recursively filter properties that match search results.
|
|
238
|
+
* @param filter - Function to filter based on search results
|
|
239
|
+
* @param props - Schema properties being filtered
|
|
240
|
+
* @param definitions - Definitions to use for filling in references in properties
|
|
241
|
+
* @param ref - Reference to a definition
|
|
242
|
+
* @returns - String array of properties that match the search results.
|
|
243
|
+
*/
|
|
244
|
+
getFilterString(
|
|
245
|
+
filter: (item: string) => Partial<IScore> | null,
|
|
246
|
+
props: ISettingRegistry.IProperty,
|
|
247
|
+
definitions?: any,
|
|
248
|
+
ref?: string
|
|
249
|
+
): string[] {
|
|
250
|
+
// If properties given are references, populate properties
|
|
251
|
+
// with corresponding definition.
|
|
252
|
+
if (ref && definitions) {
|
|
253
|
+
ref = ref.replace('#/definitions/', '');
|
|
254
|
+
props = definitions[ref] ?? {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// If given properties are an object, advance into the properties
|
|
258
|
+
// for that object instead.
|
|
259
|
+
if (props.properties) {
|
|
260
|
+
props = props.properties;
|
|
261
|
+
// If given properties are an array, advance into the properties
|
|
262
|
+
// for the items instead.
|
|
263
|
+
} else if (props.items) {
|
|
264
|
+
props = props.items as any;
|
|
265
|
+
// Otherwise, you've reached the base case and don't need to check for matching properties
|
|
266
|
+
} else {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If reference found, recurse
|
|
271
|
+
if (props['$ref']) {
|
|
272
|
+
return this.getFilterString(
|
|
273
|
+
filter,
|
|
274
|
+
props,
|
|
275
|
+
definitions,
|
|
276
|
+
props['$ref'] as string
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Make sure props is non-empty before calling reduce
|
|
281
|
+
if (Object.keys(props).length === 0) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Iterate through the properties and check for titles / descriptions that match search.
|
|
286
|
+
return Object.keys(props).reduce((acc: string[], value: any) => {
|
|
287
|
+
// If this is the base case, check for matching title / description
|
|
288
|
+
const subProps = props[value] as PartialJSONObject;
|
|
289
|
+
if (!subProps) {
|
|
290
|
+
if (filter((props.title as string) ?? '')) {
|
|
291
|
+
return props.title;
|
|
292
|
+
}
|
|
293
|
+
if (filter(value)) {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If there are properties in the object, check for title / description
|
|
299
|
+
if (filter((subProps.title as string) ?? '')) {
|
|
300
|
+
acc.push(subProps.title as string);
|
|
301
|
+
}
|
|
302
|
+
if (filter(value)) {
|
|
303
|
+
acc.push(value);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Finally, recurse on the properties left.
|
|
307
|
+
acc.concat(
|
|
308
|
+
this.getFilterString(
|
|
309
|
+
filter,
|
|
310
|
+
subProps as ISettingRegistry.IProperty,
|
|
311
|
+
definitions,
|
|
312
|
+
subProps['$ref'] as string
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
return acc;
|
|
316
|
+
}, []);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Updates the filter when the search bar value changes.
|
|
321
|
+
* @param filter Filter function passed by search bar based on search value.
|
|
322
|
+
*/
|
|
323
|
+
setFilter(
|
|
324
|
+
filter: (item: string) => Partial<IScore> | null,
|
|
325
|
+
query?: string
|
|
326
|
+
): 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
|
+
};
|
|
338
|
+
this._query = query;
|
|
339
|
+
this._updateFilterSignal.emit(this._filter);
|
|
340
|
+
this.update();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setError(id: string, error: boolean): void {
|
|
344
|
+
if (this._errors[id] !== error) {
|
|
345
|
+
this._errors[id] = error;
|
|
346
|
+
this.update();
|
|
347
|
+
} else {
|
|
348
|
+
this._errors[id] = error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
mapPlugins(plugin: ISettingRegistry.IPlugin): JSX.Element {
|
|
353
|
+
const { id, schema, version } = plugin;
|
|
354
|
+
const trans = this.translator.load('jupyterlab');
|
|
355
|
+
const title =
|
|
356
|
+
typeof schema.title === 'string' ? trans._p('schema', schema.title) : id;
|
|
357
|
+
const highlightedTitleIndices = StringExt.matchSumOfSquares(
|
|
358
|
+
title.toLocaleLowerCase(),
|
|
359
|
+
this._query?.toLocaleLowerCase() ?? ''
|
|
360
|
+
);
|
|
361
|
+
const hightlightedTitle = StringExt.highlight(
|
|
362
|
+
title,
|
|
363
|
+
highlightedTitleIndices?.indices ?? [],
|
|
364
|
+
chunk => {
|
|
365
|
+
return <mark>{chunk}</mark>;
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
const description =
|
|
369
|
+
typeof schema.description === 'string'
|
|
370
|
+
? trans._p('schema', schema.description)
|
|
371
|
+
: '';
|
|
372
|
+
const itemTitle = `${description}\n${id}\n${version}`;
|
|
373
|
+
const icon = this.getHint(ICON_KEY, this.registry, plugin);
|
|
374
|
+
const iconClass = this.getHint(ICON_CLASS_KEY, this.registry, plugin);
|
|
375
|
+
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
|
+
});
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
onClick={this._evtMousedown}
|
|
394
|
+
className={`${
|
|
395
|
+
id === this.selection
|
|
396
|
+
? 'jp-mod-selected jp-PluginList-entry'
|
|
397
|
+
: 'jp-PluginList-entry'
|
|
398
|
+
} ${this._errors[id] ? 'jp-ErrorPlugin' : ''}`}
|
|
399
|
+
data-id={id}
|
|
400
|
+
key={id}
|
|
401
|
+
title={itemTitle}
|
|
402
|
+
>
|
|
403
|
+
<div className="jp-PluginList-entry-label" role="tab">
|
|
404
|
+
<div className="jp-SelectedIndicator" />
|
|
405
|
+
<LabIcon.resolveReact
|
|
406
|
+
icon={icon || (iconClass ? undefined : settingsIcon)}
|
|
407
|
+
iconClass={classes(iconClass, 'jp-Icon')}
|
|
408
|
+
title={iconTitle}
|
|
409
|
+
tag="span"
|
|
410
|
+
stylesheet="settingsEditor"
|
|
411
|
+
/>
|
|
412
|
+
<span className="jp-PluginList-entry-label-text">
|
|
413
|
+
{hightlightedTitle}
|
|
414
|
+
</span>
|
|
415
|
+
</div>
|
|
416
|
+
<ul>{filteredProperties}</ul>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
render(): JSX.Element {
|
|
422
|
+
const trans = this.translator.load('jupyterlab');
|
|
423
|
+
// Filter all plugins based on search value before displaying list.
|
|
424
|
+
const allPlugins = this._allPlugins.filter(plugin => {
|
|
425
|
+
const filtered = this._filter(plugin);
|
|
426
|
+
return filtered === null || filtered.length > 0;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const modifiedPlugins = allPlugins.filter(plugin => {
|
|
430
|
+
return this._settings[plugin.id]?.isModified;
|
|
431
|
+
});
|
|
432
|
+
const modifiedItems = modifiedPlugins.map(this.mapPlugins);
|
|
433
|
+
const otherItems = allPlugins
|
|
434
|
+
.filter(plugin => {
|
|
435
|
+
return !modifiedPlugins.includes(plugin);
|
|
436
|
+
})
|
|
437
|
+
.map(this.mapPlugins);
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div className="jp-PluginList-wrapper">
|
|
441
|
+
<FilterBox
|
|
442
|
+
updateFilter={this.setFilter}
|
|
443
|
+
useFuzzyFilter={false}
|
|
444
|
+
placeholder={trans.__('Search…')}
|
|
445
|
+
forceRefresh={false}
|
|
446
|
+
caseSensitive={false}
|
|
447
|
+
initialQuery={this._query}
|
|
448
|
+
/>
|
|
449
|
+
{modifiedItems.length > 0 && (
|
|
450
|
+
<div>
|
|
451
|
+
<h1 className="jp-PluginList-header">{trans.__('Modified')}</h1>
|
|
452
|
+
<ul>{modifiedItems}</ul>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
{otherItems.length > 0 && (
|
|
456
|
+
<div>
|
|
457
|
+
<h1 className="jp-PluginList-header">{trans.__('Settings')}</h1>
|
|
458
|
+
<ul>{otherItems}</ul>
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
{modifiedItems.length === 0 && otherItems.length === 0 && (
|
|
462
|
+
<p className="jp-PluginList-noResults">
|
|
463
|
+
{trans.__('No items match your search.')}
|
|
464
|
+
</p>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
protected translator: ITranslator;
|
|
471
|
+
private _changed = new Signal<this, void>(this);
|
|
472
|
+
private _errors: { [id: string]: boolean };
|
|
473
|
+
private _filter: (item: ISettingRegistry.IPlugin) => string[] | null;
|
|
474
|
+
private _query: string | undefined;
|
|
475
|
+
private _handleSelectSignal = new Signal<this, string>(this);
|
|
476
|
+
private _updateFilterSignal = new Signal<
|
|
477
|
+
this,
|
|
478
|
+
(plugin: ISettingRegistry.IPlugin) => string[] | null
|
|
479
|
+
>(this);
|
|
480
|
+
private _allPlugins: ISettingRegistry.IPlugin[] = [];
|
|
481
|
+
private _settings: { [id: string]: Settings } = {};
|
|
482
|
+
private _confirm?: (id: string) => Promise<void>;
|
|
483
|
+
private _scrollTop: number | undefined = 0;
|
|
484
|
+
private _selection = '';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* A namespace for `PluginList` statics.
|
|
489
|
+
*/
|
|
490
|
+
export namespace PluginList {
|
|
491
|
+
/**
|
|
492
|
+
* The instantiation options for a plugin list.
|
|
493
|
+
*/
|
|
494
|
+
export interface IOptions {
|
|
495
|
+
/**
|
|
496
|
+
* A function that allows for asynchronously confirming a selection.
|
|
497
|
+
*
|
|
498
|
+
* #### Notes
|
|
499
|
+
* If the promise returned by the function resolves, then the selection will
|
|
500
|
+
* succeed and emit an event. If the promise rejects, the selection is not
|
|
501
|
+
* made.
|
|
502
|
+
*/
|
|
503
|
+
confirm?: (id: string) => Promise<void>;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The setting registry for the plugin list.
|
|
507
|
+
*/
|
|
508
|
+
registry: ISettingRegistry;
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* List of plugins to skip
|
|
512
|
+
*/
|
|
513
|
+
toSkip?: string[];
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The setting registry for the plugin list.
|
|
517
|
+
*/
|
|
518
|
+
translator?: ITranslator;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* An optional initial query so the plugin list can filter on start.
|
|
522
|
+
*/
|
|
523
|
+
query?: string;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Sort a list of plugins by title and ID.
|
|
528
|
+
*/
|
|
529
|
+
export function sortPlugins(
|
|
530
|
+
registry: ISettingRegistry
|
|
531
|
+
): ISettingRegistry.IPlugin[] {
|
|
532
|
+
return Object.keys(registry.plugins)
|
|
533
|
+
.map(plugin => registry.plugins[plugin]!)
|
|
534
|
+
.sort((a, b) => {
|
|
535
|
+
return (a.schema.title || a.id).localeCompare(b.schema.title || b.id);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|