@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.
Files changed (47) hide show
  1. package/lib/SettingsFormEditor.d.ts +101 -0
  2. package/lib/SettingsFormEditor.js +151 -0
  3. package/lib/SettingsFormEditor.js.map +1 -0
  4. package/lib/index.d.ts +2 -1
  5. package/lib/index.js +2 -1
  6. package/lib/index.js.map +1 -1
  7. package/lib/inspector.js +24 -30
  8. package/lib/inspector.js.map +1 -1
  9. package/lib/{settingeditor.d.ts → jsonsettingeditor.d.ts} +5 -6
  10. package/lib/{settingeditor.js → jsonsettingeditor.js} +25 -43
  11. package/lib/jsonsettingeditor.js.map +1 -0
  12. package/lib/plugineditor.d.ts +3 -3
  13. package/lib/plugineditor.js +2 -6
  14. package/lib/plugineditor.js.map +1 -1
  15. package/lib/pluginlist.d.ts +58 -22
  16. package/lib/pluginlist.js +246 -109
  17. package/lib/pluginlist.js.map +1 -1
  18. package/lib/raweditor.d.ts +2 -6
  19. package/lib/raweditor.js +22 -33
  20. package/lib/raweditor.js.map +1 -1
  21. package/lib/settingseditor.d.ts +81 -0
  22. package/lib/settingseditor.js +121 -0
  23. package/lib/settingseditor.js.map +1 -0
  24. package/lib/settingspanel.d.ts +54 -0
  25. package/lib/settingspanel.js +102 -0
  26. package/lib/settingspanel.js.map +1 -0
  27. package/lib/tokens.d.ts +12 -2
  28. package/lib/tokens.js +4 -1
  29. package/lib/tokens.js.map +1 -1
  30. package/package.json +28 -21
  31. package/src/SettingsFormEditor.tsx +306 -0
  32. package/src/index.ts +10 -0
  33. package/src/inspector.ts +144 -0
  34. package/src/jsonsettingeditor.tsx +482 -0
  35. package/src/plugineditor.ts +257 -0
  36. package/src/pluginlist.tsx +538 -0
  37. package/src/raweditor.ts +422 -0
  38. package/src/settingseditor.tsx +210 -0
  39. package/src/settingspanel.tsx +218 -0
  40. package/src/tokens.ts +33 -0
  41. package/style/base.css +213 -89
  42. package/style/index.css +1 -0
  43. package/style/index.js +1 -0
  44. package/lib/settingeditor.js.map +0 -1
  45. package/lib/splitpanel.d.ts +0 -13
  46. package/lib/splitpanel.js +0 -26
  47. package/lib/splitpanel.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlab/settingeditor",
3
- "version": "4.0.0-alpha.2",
3
+ "version": "4.0.0-alpha.20",
4
4
  "description": "The JupyterLab default setting editor interface",
5
5
  "homepage": "https://github.com/jupyterlab/jupyterlab",
6
6
  "bugs": {
@@ -27,7 +27,8 @@
27
27
  "lib/*.js.map",
28
28
  "lib/*.js",
29
29
  "style/*.css",
30
- "style/index.js"
30
+ "style/index.js",
31
+ "src/**/*.{ts,tsx}"
31
32
  ],
32
33
  "scripts": {
33
34
  "build": "tsc -b",
@@ -36,28 +37,34 @@
36
37
  "watch": "tsc -b --watch"
37
38
  },
38
39
  "dependencies": {
39
- "@jupyterlab/apputils": "^4.0.0-alpha.2",
40
- "@jupyterlab/codeeditor": "^4.0.0-alpha.2",
41
- "@jupyterlab/inspector": "^4.0.0-alpha.2",
42
- "@jupyterlab/rendermime": "^4.0.0-alpha.2",
43
- "@jupyterlab/settingregistry": "^4.0.0-alpha.2",
44
- "@jupyterlab/statedb": "^4.0.0-alpha.2",
45
- "@jupyterlab/translation": "^4.0.0-alpha.2",
46
- "@jupyterlab/ui-components": "^4.0.0-alpha.17",
47
- "@lumino/commands": "^1.19.0",
48
- "@lumino/coreutils": "^1.11.1",
49
- "@lumino/messaging": "^1.10.1",
50
- "@lumino/signaling": "^1.10.1",
51
- "@lumino/widgets": "^1.30.0",
52
- "react": "^17.0.1",
53
- "react-dom": "^17.0.1"
40
+ "@jupyterlab/application": "^4.0.0-alpha.20",
41
+ "@jupyterlab/apputils": "^4.0.0-alpha.20",
42
+ "@jupyterlab/codeeditor": "^4.0.0-alpha.20",
43
+ "@jupyterlab/inspector": "^4.0.0-alpha.20",
44
+ "@jupyterlab/rendermime": "^4.0.0-alpha.20",
45
+ "@jupyterlab/settingregistry": "^4.0.0-alpha.20",
46
+ "@jupyterlab/statedb": "^4.0.0-alpha.20",
47
+ "@jupyterlab/translation": "^4.0.0-alpha.20",
48
+ "@jupyterlab/ui-components": "^4.0.0-alpha.35",
49
+ "@lumino/algorithm": "^2.0.0-rc.0",
50
+ "@lumino/commands": "^2.0.0-rc.0",
51
+ "@lumino/coreutils": "^2.0.0-rc.0",
52
+ "@lumino/disposable": "^2.0.0-rc.0",
53
+ "@lumino/messaging": "^2.0.0-rc.0",
54
+ "@lumino/polling": "^2.0.0-rc.0",
55
+ "@lumino/signaling": "^2.0.0-rc.0",
56
+ "@lumino/widgets": "^2.0.0-rc.0",
57
+ "@rjsf/core": "^5.1.0",
58
+ "@rjsf/utils": "^5.1.0",
59
+ "@rjsf/validator-ajv8": "^5.1.0",
60
+ "json-schema": "^0.4.0",
61
+ "react": "^18.2.0"
54
62
  },
55
63
  "devDependencies": {
56
- "@types/react": "^17.0.0",
57
- "@types/react-dom": "^17.0.0",
64
+ "@types/react": "^18.0.26",
58
65
  "rimraf": "~3.0.0",
59
- "typedoc": "~0.21.2",
60
- "typescript": "~4.5.2"
66
+ "typedoc": "~0.23.25",
67
+ "typescript": "~5.0.0-beta"
61
68
  },
62
69
  "publishConfig": {
63
70
  "access": "public"
@@ -0,0 +1,306 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import { showErrorMessage } from '@jupyterlab/apputils';
7
+ import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
8
+ import { ITranslator } from '@jupyterlab/translation';
9
+ import {
10
+ caretDownIcon,
11
+ caretRightIcon,
12
+ FormComponent
13
+ } from '@jupyterlab/ui-components';
14
+ import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils';
15
+ import { Debouncer } from '@lumino/polling';
16
+ import { IChangeEvent } from '@rjsf/core';
17
+ import validatorAjv8 from '@rjsf/validator-ajv8';
18
+ import { Field, UiSchema } from '@rjsf/utils';
19
+ import { JSONSchema7 } from 'json-schema';
20
+ import React from 'react';
21
+
22
+ /**
23
+ * Indentation to use when saving the settings as JSON document.
24
+ */
25
+ const JSON_INDENTATION = 4;
26
+
27
+ /**
28
+ * Namespace for a React component that prepares the settings for a
29
+ * given plugin to be rendered in the FormEditor.
30
+ */
31
+ export namespace SettingsFormEditor {
32
+ /**
33
+ * Props passed to the SettingsFormEditor component
34
+ */
35
+ export interface IProps {
36
+ /**
37
+ * Settings object with schema and user defined values.
38
+ */
39
+ settings: Settings;
40
+
41
+ /**
42
+ * Dictionary used for custom field renderers in the form.
43
+ */
44
+ renderers: { [id: string]: { [property: string]: Field } };
45
+
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
+ /**
57
+ * Translator object
58
+ */
59
+ translator: ITranslator;
60
+
61
+ /**
62
+ * Callback to update the plugin list when a validation error occurs.
63
+ */
64
+ hasError: (error: boolean) => void;
65
+
66
+ /**
67
+ * Handler for when selection change is triggered by scrolling
68
+ * in the SettingsPanel.
69
+ */
70
+ onSelect: (id: string) => void;
71
+
72
+ /**
73
+ * Sends whether this editor has unsaved changes to the parent class.
74
+ */
75
+ updateDirtyState: (dirty: boolean) => void;
76
+
77
+ /**
78
+ * List of strings that match search value.
79
+ */
80
+ filteredValues: string[] | null;
81
+ }
82
+
83
+ export interface IState {
84
+ /**
85
+ * Indicates whether the settings have been modified. Used for hiding
86
+ * the "Restore to Default" button when there are no changes.
87
+ */
88
+ isModified: boolean;
89
+
90
+ // The following are state derived from props for memoization to avoid
91
+ // `Form` update that results in focus lost
92
+ // A better fix (that will break the API) would be to move this as props
93
+ // of the component
94
+ /**
95
+ * Form UI schema
96
+ */
97
+ uiSchema: UiSchema;
98
+ /**
99
+ * Filtered schema
100
+ */
101
+ filteredSchema?: ISettingRegistry.ISchema;
102
+ /**
103
+ * Form context
104
+ */
105
+ formContext?: any;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * A React component that prepares the settings for a
111
+ * given plugin to be rendered in the FormEditor.
112
+ */
113
+ export class SettingsFormEditor extends React.Component<
114
+ SettingsFormEditor.IProps,
115
+ SettingsFormEditor.IState
116
+ > {
117
+ constructor(props: SettingsFormEditor.IProps) {
118
+ super(props);
119
+ const { settings } = props;
120
+ this._formData = settings.composite;
121
+ this.state = {
122
+ isModified: settings.isModified,
123
+ uiSchema: {},
124
+ filteredSchema: this.props.settings.schema,
125
+ formContext: {
126
+ defaultFormData: this.props.settings.default(),
127
+ settings: this.props.settings
128
+ }
129
+ };
130
+ this.handleChange = this.handleChange.bind(this);
131
+ this._debouncer = new Debouncer(this.handleChange);
132
+ }
133
+
134
+ componentDidMount(): void {
135
+ this._setUiSchema();
136
+ this._setFilteredSchema();
137
+ }
138
+
139
+ componentDidUpdate(prevProps: SettingsFormEditor.IProps): void {
140
+ this._setUiSchema(prevProps.renderers[prevProps.settings.id]);
141
+ this._setFilteredSchema(prevProps.filteredValues);
142
+
143
+ if (prevProps.settings !== this.props.settings) {
144
+ this.setState({
145
+ formContext: {
146
+ settings: this.props.settings,
147
+ defaultFormData: this.props.settings.default()
148
+ }
149
+ });
150
+ }
151
+ }
152
+
153
+ componentWillUnmount(): void {
154
+ this._debouncer.dispose();
155
+ }
156
+
157
+ /**
158
+ * Handler for edits made in the form editor.
159
+ */
160
+ handleChange(): void {
161
+ // Prevent unnecessary save when opening settings that haven't been modified.
162
+ if (
163
+ !this.props.settings.isModified &&
164
+ this.props.settings.isDefault(this._formData)
165
+ ) {
166
+ this.props.updateDirtyState(false);
167
+ return;
168
+ }
169
+ this.props.settings
170
+ .save(JSON.stringify(this._formData, undefined, JSON_INDENTATION))
171
+ .then(() => {
172
+ this.props.updateDirtyState(false);
173
+ this.setState({ isModified: this.props.settings.isModified });
174
+ })
175
+ .catch((reason: string) => {
176
+ this.props.updateDirtyState(false);
177
+ const trans = this.props.translator.load('jupyterlab');
178
+ void showErrorMessage(trans.__('Error saving settings.'), reason);
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Handler for the "Restore to defaults" button - clears all
184
+ * modified settings then calls `setFormData` to restore the
185
+ * values.
186
+ */
187
+ reset = async (event: React.MouseEvent): Promise<void> => {
188
+ event.stopPropagation();
189
+ for (const field in this.props.settings.user) {
190
+ await this.props.settings.remove(field);
191
+ }
192
+ this._formData = this.props.settings.composite;
193
+ this.setState({ isModified: false });
194
+ };
195
+
196
+ render(): JSX.Element {
197
+ const trans = this.props.translator.load('jupyterlab');
198
+ const icon = this.props.isCollapsed ? caretRightIcon : caretDownIcon;
199
+
200
+ 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
+ )}
225
+ </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>
241
+ );
242
+ }
243
+
244
+ private _onChange = (e: IChangeEvent<ReadonlyPartialJSONObject>): void => {
245
+ this.props.hasError(e.errors.length !== 0);
246
+ this._formData = e.formData;
247
+ if (e.errors.length === 0) {
248
+ this.props.updateDirtyState(true);
249
+ void this._debouncer.invoke();
250
+ }
251
+ this.props.onSelect(this.props.settings.id);
252
+ };
253
+
254
+ private _setUiSchema(prevRenderers?: { [id: string]: Field }) {
255
+ const renderers = this.props.renderers[this.props.settings.id];
256
+ if (
257
+ !JSONExt.deepEqual(
258
+ Object.keys(prevRenderers ?? {}).sort(),
259
+ Object.keys(renderers ?? {}).sort()
260
+ )
261
+ ) {
262
+ /**
263
+ * Construct uiSchema to pass any custom renderers to the form editor.
264
+ */
265
+ const uiSchema: UiSchema = {};
266
+ for (const id in this.props.renderers[this.props.settings.id]) {
267
+ if (
268
+ Object.keys(this.props.settings.schema.properties ?? {}).includes(id)
269
+ ) {
270
+ uiSchema[id] = {
271
+ 'ui:field': id
272
+ };
273
+ }
274
+ }
275
+ this.setState({ uiSchema });
276
+ }
277
+ }
278
+
279
+ private _setFilteredSchema(prevFilteredValues?: string[] | null) {
280
+ if (
281
+ prevFilteredValues === undefined ||
282
+ !JSONExt.deepEqual(prevFilteredValues, this.props.filteredValues)
283
+ ) {
284
+ /**
285
+ * Only show fields that match search value.
286
+ */
287
+ const filteredSchema = JSONExt.deepCopy(this.props.settings.schema);
288
+ if (this.props.filteredValues?.length ?? 0 > 0) {
289
+ for (const field in filteredSchema.properties) {
290
+ if (
291
+ !this.props.filteredValues?.includes(
292
+ filteredSchema.properties[field].title ?? field
293
+ )
294
+ ) {
295
+ delete filteredSchema.properties[field];
296
+ }
297
+ }
298
+ }
299
+
300
+ this.setState({ filteredSchema });
301
+ }
302
+ }
303
+
304
+ private _debouncer: Debouncer<void, any>;
305
+ private _formData: any;
306
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+ /**
4
+ * @packageDocumentation
5
+ * @module settingeditor
6
+ */
7
+
8
+ export * from './settingseditor';
9
+ export * from './jsonsettingeditor';
10
+ export * from './tokens';
@@ -0,0 +1,144 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import { InspectionHandler, InspectorPanel } from '@jupyterlab/inspector';
7
+ import {
8
+ IRenderMimeRegistry,
9
+ RenderMimeRegistry,
10
+ standardRendererFactories
11
+ } from '@jupyterlab/rendermime';
12
+ import { ISchemaValidator } from '@jupyterlab/settingregistry';
13
+ import { DataConnector } from '@jupyterlab/statedb';
14
+ import {
15
+ ITranslator,
16
+ nullTranslator,
17
+ TranslationBundle
18
+ } from '@jupyterlab/translation';
19
+ import { ReadonlyJSONObject } from '@lumino/coreutils';
20
+ import { RawEditor } from './raweditor';
21
+
22
+ /**
23
+ * Create a raw editor inspector.
24
+ */
25
+ export function createInspector(
26
+ editor: RawEditor,
27
+ rendermime?: IRenderMimeRegistry,
28
+ translator?: ITranslator
29
+ ): InspectorPanel {
30
+ translator = translator || nullTranslator;
31
+ const trans = translator.load('jupyterlab');
32
+ const connector = new InspectorConnector(editor, translator);
33
+ const inspector = new InspectorPanel({
34
+ initialContent: trans.__('Any errors will be listed here'),
35
+ translator: translator
36
+ });
37
+ const handler = new InspectionHandler({
38
+ connector,
39
+ rendermime:
40
+ rendermime ||
41
+ new RenderMimeRegistry({
42
+ initialFactories: standardRendererFactories,
43
+ translator: translator
44
+ })
45
+ });
46
+
47
+ inspector.addClass('jp-SettingsDebug');
48
+ inspector.source = handler;
49
+ handler.editor = editor.source;
50
+
51
+ return inspector;
52
+ }
53
+
54
+ /**
55
+ * The data connector used to populate a code inspector.
56
+ *
57
+ * #### Notes
58
+ * This data connector debounces fetch requests to throttle them at no more than
59
+ * one request per 100ms. This means that using the connector to populate
60
+ * multiple client objects can lead to missed fetch responses.
61
+ */
62
+ class InspectorConnector extends DataConnector<
63
+ InspectionHandler.IReply,
64
+ void,
65
+ InspectionHandler.IRequest
66
+ > {
67
+ constructor(editor: RawEditor, translator?: ITranslator) {
68
+ super();
69
+ this._editor = editor;
70
+ this._trans = (translator ?? nullTranslator).load('jupyterlab');
71
+ }
72
+
73
+ /**
74
+ * Fetch inspection requests.
75
+ */
76
+ fetch(
77
+ request: InspectionHandler.IRequest
78
+ ): Promise<InspectionHandler.IReply | undefined> {
79
+ return new Promise<InspectionHandler.IReply | undefined>(resolve => {
80
+ // Debounce requests at a rate of 100ms.
81
+ const current = (this._current = window.setTimeout(() => {
82
+ if (current !== this._current) {
83
+ return resolve(undefined);
84
+ }
85
+
86
+ const errors = this._validate(request.text);
87
+
88
+ if (!errors) {
89
+ return resolve({
90
+ data: { 'text/markdown': this._trans.__('No errors found') },
91
+ metadata: {}
92
+ });
93
+ }
94
+
95
+ resolve({ data: this.render(errors), metadata: {} });
96
+ }, 100));
97
+ });
98
+ }
99
+ /**
100
+ * Render validation errors as an HTML string.
101
+ */
102
+ protected render(errors: ISchemaValidator.IError[]): ReadonlyJSONObject {
103
+ return {
104
+ 'text/markdown': errors.map(this.renderError.bind(this)).join('')
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Render an individual validation error as a markdown string.
110
+ */
111
+ protected renderError(error: ISchemaValidator.IError): string {
112
+ switch (error.keyword) {
113
+ case 'additionalProperties':
114
+ return `**\`[${this._trans.__('additional property error')}]\`**
115
+ ${this._trans.__(
116
+ '`%1` is not a valid property',
117
+ error.params?.additionalProperty
118
+ )}`;
119
+ case 'syntax':
120
+ return `**\`[${this._trans.__('syntax error')}]\`** *${error.message}*`;
121
+ case 'type':
122
+ return `**\`[${this._trans.__('type error')}]\`**
123
+ \`${error.instancePath}\` ${error.message}`;
124
+ default:
125
+ return `**\`[${this._trans.__('error')}]\`** *${error.message}*`;
126
+ }
127
+ }
128
+
129
+ private _validate(raw: string): ISchemaValidator.IError[] | null {
130
+ const editor = this._editor;
131
+ if (!editor.settings) {
132
+ return null;
133
+ }
134
+ const { id, schema, version } = editor.settings;
135
+ const data = { composite: {}, user: {} };
136
+ const validator = editor.registry.validator;
137
+
138
+ return validator.validateData({ data, id, raw, schema, version }, false);
139
+ }
140
+
141
+ private _trans: TranslationBundle;
142
+ private _current = 0;
143
+ private _editor: RawEditor;
144
+ }