@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.
@@ -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
  }