@jupyterlab/settingeditor 4.0.0-alpha.19 → 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 +0 -1
- package/lib/SettingsFormEditor.js +0 -1
- package/lib/SettingsFormEditor.js.map +1 -1
- package/lib/raweditor.js +9 -7
- package/lib/raweditor.js.map +1 -1
- package/package.json +22 -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 +4 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
8
|
+
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
|
|
9
|
+
import { ISignal } from '@lumino/signaling';
|
|
10
|
+
import type { Field } from '@rjsf/utils';
|
|
11
|
+
import React, { useEffect, useState } from 'react';
|
|
12
|
+
import { PluginList } from './pluginlist';
|
|
13
|
+
import { SettingsFormEditor } from './SettingsFormEditor';
|
|
14
|
+
|
|
15
|
+
export interface ISettingsPanelProps {
|
|
16
|
+
/**
|
|
17
|
+
* List of Settings objects that provide schema and values
|
|
18
|
+
* of plugins.
|
|
19
|
+
*/
|
|
20
|
+
settings: Settings[];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Form component registry that provides renderers
|
|
24
|
+
* for the form editor.
|
|
25
|
+
*/
|
|
26
|
+
editorRegistry: IFormRendererRegistry;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handler for when selection change is triggered by scrolling
|
|
30
|
+
* in the SettingsPanel.
|
|
31
|
+
*/
|
|
32
|
+
onSelect: (id: string) => void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Signal that fires when a selection is made in the plugin list.
|
|
36
|
+
*/
|
|
37
|
+
handleSelectSignal: ISignal<PluginList, string>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Translator object
|
|
41
|
+
*/
|
|
42
|
+
translator: ITranslator;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Callback to update the plugin list to display plugins with
|
|
46
|
+
* invalid / unsaved settings in red.
|
|
47
|
+
*/
|
|
48
|
+
hasError: (id: string, error: boolean) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sends the updated dirty state to the parent class.
|
|
52
|
+
*/
|
|
53
|
+
updateDirtyState: (dirty: boolean) => void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Signal that sends updated filter when search value changes.
|
|
57
|
+
*/
|
|
58
|
+
updateFilterSignal: ISignal<
|
|
59
|
+
PluginList,
|
|
60
|
+
(plugin: ISettingRegistry.IPlugin) => string[] | null
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* If the settings editor is created with an initial search query, an initial
|
|
65
|
+
* filter function is passed to the settings panel.
|
|
66
|
+
*/
|
|
67
|
+
initialFilter: (item: ISettingRegistry.IPlugin) => string[] | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* React component that displays a list of SettingsFormEditor
|
|
72
|
+
* components.
|
|
73
|
+
*/
|
|
74
|
+
export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
|
|
75
|
+
settings,
|
|
76
|
+
editorRegistry,
|
|
77
|
+
onSelect,
|
|
78
|
+
handleSelectSignal,
|
|
79
|
+
hasError,
|
|
80
|
+
updateDirtyState,
|
|
81
|
+
updateFilterSignal,
|
|
82
|
+
translator,
|
|
83
|
+
initialFilter
|
|
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
|
+
}
|
|
97
|
+
const wrapperRef: React.RefObject<HTMLDivElement> = React.useRef(null);
|
|
98
|
+
const editorDirtyStates: React.RefObject<{
|
|
99
|
+
[id: string]: boolean;
|
|
100
|
+
}> = React.useRef({});
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const onFilterUpdate = (
|
|
104
|
+
list: PluginList,
|
|
105
|
+
newFilter: (plugin: ISettingRegistry.IPlugin) => string[] | null
|
|
106
|
+
) => {
|
|
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
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
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
|
+
// When filter updates, only show plugins that match search.
|
|
127
|
+
updateFilterSignal.connect(onFilterUpdate);
|
|
128
|
+
|
|
129
|
+
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);
|
|
133
|
+
};
|
|
134
|
+
handleSelectSignal?.connect?.(onSelectChange);
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
updateFilterSignal.disconnect(onFilterUpdate);
|
|
138
|
+
handleSelectSignal?.disconnect?.(onSelectChange);
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const updateDirtyStates = React.useCallback(
|
|
143
|
+
(id: string, dirty: boolean) => {
|
|
144
|
+
if (editorDirtyStates.current) {
|
|
145
|
+
editorDirtyStates.current[id] = dirty;
|
|
146
|
+
for (const editor in editorDirtyStates.current) {
|
|
147
|
+
if (editorDirtyStates.current[editor]) {
|
|
148
|
+
updateDirtyState(true);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
updateDirtyState(false);
|
|
154
|
+
},
|
|
155
|
+
[editorDirtyStates, updateDirtyState]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const renderers = React.useMemo(
|
|
159
|
+
() =>
|
|
160
|
+
Object.entries(editorRegistry.renderers).reduce<{
|
|
161
|
+
[plugin: string]: { [property: string]: Field };
|
|
162
|
+
}>((agg, [id, renderer]) => {
|
|
163
|
+
const splitPosition = id.lastIndexOf('.');
|
|
164
|
+
const pluginId = id.substring(0, splitPosition);
|
|
165
|
+
const propertyName = id.substring(splitPosition + 1);
|
|
166
|
+
if (!agg[pluginId]) {
|
|
167
|
+
agg[pluginId] = {};
|
|
168
|
+
}
|
|
169
|
+
if (!agg[pluginId][propertyName] && renderer.fieldRenderer) {
|
|
170
|
+
agg[pluginId][propertyName] = renderer.fieldRenderer;
|
|
171
|
+
}
|
|
172
|
+
return agg;
|
|
173
|
+
}, {}),
|
|
174
|
+
[editorRegistry]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="jp-SettingsPanel" ref={wrapperRef}>
|
|
179
|
+
{settings.map(pluginSettings => {
|
|
180
|
+
// Pass filtered results to SettingsFormEditor to only display filtered fields.
|
|
181
|
+
const filtered = filterPlugin(pluginSettings.plugin);
|
|
182
|
+
// If filtered results are an array, only show if the array is non-empty.
|
|
183
|
+
if (filtered !== null && filtered.length === 0) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
ref={editorRefs[pluginSettings.id]}
|
|
189
|
+
className="jp-SettingsForm"
|
|
190
|
+
key={`${pluginSettings.id}SettingsEditor`}
|
|
191
|
+
>
|
|
192
|
+
<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
|
+
filteredValues={filtered}
|
|
202
|
+
settings={pluginSettings}
|
|
203
|
+
renderers={renderers}
|
|
204
|
+
hasError={(error: boolean) => {
|
|
205
|
+
hasError(pluginSettings.id, error);
|
|
206
|
+
}}
|
|
207
|
+
updateDirtyState={(dirty: boolean) => {
|
|
208
|
+
updateDirtyStates(pluginSettings.id, dirty);
|
|
209
|
+
}}
|
|
210
|
+
onSelect={onSelect}
|
|
211
|
+
translator={translator}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
import { JsonSettingEditor as JSONSettingEditor } from './jsonsettingeditor';
|
|
7
|
+
import { SettingsEditor } from './settingseditor';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The setting editor tracker token.
|
|
11
|
+
*/
|
|
12
|
+
export const ISettingEditorTracker = new Token<ISettingEditorTracker>(
|
|
13
|
+
'@jupyterlab/settingeditor:ISettingEditorTracker'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The setting editor tracker token.
|
|
18
|
+
*/
|
|
19
|
+
export const IJSONSettingEditorTracker = new Token<IJSONSettingEditorTracker>(
|
|
20
|
+
'@jupyterlab/settingeditor:IJSONSettingEditorTracker'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A class that tracks the setting editor.
|
|
25
|
+
*/
|
|
26
|
+
export interface IJSONSettingEditorTracker
|
|
27
|
+
extends IWidgetTracker<MainAreaWidget<JSONSettingEditor>> {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A class that tracks the setting editor.
|
|
31
|
+
*/
|
|
32
|
+
export interface ISettingEditorTracker
|
|
33
|
+
extends IWidgetTracker<MainAreaWidget<SettingsEditor>> {}
|
package/style/base.css
CHANGED
|
@@ -206,6 +206,10 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
|
|
|
206
206
|
text-align: right;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
.jp-SettingsRawEditor .cm-editor {
|
|
210
|
+
height: 100%;
|
|
211
|
+
}
|
|
212
|
+
|
|
209
213
|
.jp-SettingsPanel .checkbox p {
|
|
210
214
|
font-size: var(--jp-content-font-size1);
|
|
211
215
|
}
|