@jupyterlab/apputils-extension 4.0.0-alpha.9 → 4.0.0-beta.1
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/announcements.d.ts +2 -0
- package/lib/announcements.js +227 -0
- package/lib/announcements.js.map +1 -0
- package/lib/index.js +66 -12
- package/lib/index.js.map +1 -1
- package/lib/notificationplugin.d.ts +5 -0
- package/lib/notificationplugin.js +504 -0
- package/lib/notificationplugin.js.map +1 -0
- package/lib/palette.js +3 -3
- package/lib/palette.js.map +1 -1
- package/lib/settingconnector.d.ts +4 -1
- package/lib/settingconnector.js +8 -1
- package/lib/settingconnector.js.map +1 -1
- package/lib/settingsplugin.js +6 -5
- package/lib/settingsplugin.js.map +1 -1
- package/lib/shortcuts.d.ts +21 -0
- package/lib/shortcuts.js +137 -0
- package/lib/shortcuts.js.map +1 -0
- package/lib/statusbarplugin.js +10 -9
- package/lib/statusbarplugin.js.map +1 -1
- package/lib/themesplugins.js +24 -0
- package/lib/themesplugins.js.map +1 -1
- package/lib/toolbarregistryplugin.js +4 -0
- package/lib/toolbarregistryplugin.js.map +1 -1
- package/lib/workspacesplugin.js +6 -6
- package/lib/workspacesplugin.js.map +1 -1
- package/package.json +28 -23
- package/schema/notification.json +48 -0
- package/schema/palette.json +2 -0
- package/schema/sanitizer.json +25 -0
- package/schema/utilityCommands.json +35 -0
- package/src/announcements.ts +297 -0
- package/src/index.ts +721 -0
- package/src/notificationplugin.tsx +902 -0
- package/src/palette.ts +213 -0
- package/src/settingconnector.ts +73 -0
- package/src/settingsplugin.ts +66 -0
- package/src/shortcuts.tsx +191 -0
- package/src/statusbarplugin.ts +145 -0
- package/src/themesplugins.ts +290 -0
- package/src/toolbarregistryplugin.ts +29 -0
- package/src/typings.d.ts +9 -0
- package/src/workspacesplugin.ts +306 -0
- package/style/base.css +2 -1
- package/style/contextualhelp.css +42 -0
- package/style/notification.css +227 -0
- package/style/scrollbar.raw.css +64 -0
- package/style/redirect.css +0 -15
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
JupyterFrontEnd,
|
|
8
|
+
JupyterFrontEndPlugin
|
|
9
|
+
} from '@jupyterlab/application';
|
|
10
|
+
import {
|
|
11
|
+
ICommandPalette,
|
|
12
|
+
ISplashScreen,
|
|
13
|
+
IThemeManager,
|
|
14
|
+
ThemeManager
|
|
15
|
+
} from '@jupyterlab/apputils';
|
|
16
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
17
|
+
import { IMainMenu } from '@jupyterlab/mainmenu';
|
|
18
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
19
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
20
|
+
|
|
21
|
+
import scrollbarStyleText from '../style/scrollbar.raw.css';
|
|
22
|
+
|
|
23
|
+
namespace CommandIDs {
|
|
24
|
+
export const changeTheme = 'apputils:change-theme';
|
|
25
|
+
|
|
26
|
+
export const themeScrollbars = 'apputils:theme-scrollbars';
|
|
27
|
+
|
|
28
|
+
export const changeFont = 'apputils:change-font';
|
|
29
|
+
|
|
30
|
+
export const incrFontSize = 'apputils:incr-font-size';
|
|
31
|
+
|
|
32
|
+
export const decrFontSize = 'apputils:decr-font-size';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createStyleSheet(text: string): HTMLStyleElement {
|
|
36
|
+
const style = document.createElement('style');
|
|
37
|
+
style.setAttribute('type', 'text/css');
|
|
38
|
+
style.appendChild(document.createTextNode(text));
|
|
39
|
+
return style;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The default theme manager provider.
|
|
44
|
+
*/
|
|
45
|
+
export const themesPlugin: JupyterFrontEndPlugin<IThemeManager> = {
|
|
46
|
+
id: '@jupyterlab/apputils-extension:themes',
|
|
47
|
+
requires: [ISettingRegistry, JupyterFrontEnd.IPaths, ITranslator],
|
|
48
|
+
optional: [ISplashScreen],
|
|
49
|
+
activate: (
|
|
50
|
+
app: JupyterFrontEnd,
|
|
51
|
+
settings: ISettingRegistry,
|
|
52
|
+
paths: JupyterFrontEnd.IPaths,
|
|
53
|
+
translator: ITranslator,
|
|
54
|
+
splash: ISplashScreen | null
|
|
55
|
+
): IThemeManager => {
|
|
56
|
+
const trans = translator.load('jupyterlab');
|
|
57
|
+
const host = app.shell;
|
|
58
|
+
const commands = app.commands;
|
|
59
|
+
const url = URLExt.join(PageConfig.getBaseUrl(), paths.urls.themes);
|
|
60
|
+
const key = themesPlugin.id;
|
|
61
|
+
const manager = new ThemeManager({
|
|
62
|
+
key,
|
|
63
|
+
host,
|
|
64
|
+
settings,
|
|
65
|
+
splash: splash ?? undefined,
|
|
66
|
+
url
|
|
67
|
+
});
|
|
68
|
+
let scrollbarsStyleElement: HTMLStyleElement | null = null;
|
|
69
|
+
|
|
70
|
+
// Keep a synchronously set reference to the current theme,
|
|
71
|
+
// since the asynchronous setting of the theme in `changeTheme`
|
|
72
|
+
// can lead to an incorrect toggle on the currently used theme.
|
|
73
|
+
let currentTheme: string;
|
|
74
|
+
|
|
75
|
+
manager.themeChanged.connect((sender, args) => {
|
|
76
|
+
// Set data attributes on the application shell for the current theme.
|
|
77
|
+
currentTheme = args.newValue;
|
|
78
|
+
document.body.dataset.jpThemeLight = String(
|
|
79
|
+
manager.isLight(currentTheme)
|
|
80
|
+
);
|
|
81
|
+
document.body.dataset.jpThemeName = currentTheme;
|
|
82
|
+
if (
|
|
83
|
+
document.body.dataset.jpThemeScrollbars !==
|
|
84
|
+
String(manager.themeScrollbars(currentTheme))
|
|
85
|
+
) {
|
|
86
|
+
document.body.dataset.jpThemeScrollbars = String(
|
|
87
|
+
manager.themeScrollbars(currentTheme)
|
|
88
|
+
);
|
|
89
|
+
if (manager.themeScrollbars(currentTheme)) {
|
|
90
|
+
if (!scrollbarsStyleElement) {
|
|
91
|
+
scrollbarsStyleElement = createStyleSheet(scrollbarStyleText);
|
|
92
|
+
}
|
|
93
|
+
if (!scrollbarsStyleElement.parentElement) {
|
|
94
|
+
document.body.appendChild(scrollbarsStyleElement);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (scrollbarsStyleElement && scrollbarsStyleElement.parentElement) {
|
|
98
|
+
scrollbarsStyleElement.parentElement.removeChild(
|
|
99
|
+
scrollbarsStyleElement
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
commands.notifyCommandChanged(CommandIDs.changeTheme);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
commands.addCommand(CommandIDs.changeTheme, {
|
|
109
|
+
label: args => {
|
|
110
|
+
if (args.theme === undefined) {
|
|
111
|
+
return trans.__('Switch to the provided `theme`.');
|
|
112
|
+
}
|
|
113
|
+
const theme = args['theme'] as string;
|
|
114
|
+
const displayName = manager.getDisplayName(theme);
|
|
115
|
+
return args['isPalette']
|
|
116
|
+
? trans.__('Use Theme: %1', displayName)
|
|
117
|
+
: displayName;
|
|
118
|
+
},
|
|
119
|
+
isToggled: args => args['theme'] === currentTheme,
|
|
120
|
+
execute: args => {
|
|
121
|
+
const theme = args['theme'] as string;
|
|
122
|
+
if (theme === manager.theme) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return manager.setTheme(theme);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
commands.addCommand(CommandIDs.themeScrollbars, {
|
|
130
|
+
label: trans.__('Theme Scrollbars'),
|
|
131
|
+
isToggled: () => manager.isToggledThemeScrollbars(),
|
|
132
|
+
execute: () => manager.toggleThemeScrollbars()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
commands.addCommand(CommandIDs.changeFont, {
|
|
136
|
+
label: args =>
|
|
137
|
+
args['enabled'] ? `${args['font']}` : trans.__('waiting for fonts'),
|
|
138
|
+
isEnabled: args => args['enabled'] as boolean,
|
|
139
|
+
isToggled: args => manager.getCSS(args['key'] as string) === args['font'],
|
|
140
|
+
execute: args =>
|
|
141
|
+
manager.setCSSOverride(args['key'] as string, args['font'] as string)
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
commands.addCommand(CommandIDs.incrFontSize, {
|
|
145
|
+
label: args => {
|
|
146
|
+
switch (args.key) {
|
|
147
|
+
case 'code-font-size':
|
|
148
|
+
return trans.__('Increase Code Font Size');
|
|
149
|
+
case 'content-font-size1':
|
|
150
|
+
return trans.__('Increase Content Font Size');
|
|
151
|
+
case 'ui-font-size1':
|
|
152
|
+
return trans.__('Increase UI Font Size');
|
|
153
|
+
default:
|
|
154
|
+
return trans.__('Increase Font Size');
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
execute: args => manager.incrFontSize(args['key'] as string)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
commands.addCommand(CommandIDs.decrFontSize, {
|
|
161
|
+
label: args => {
|
|
162
|
+
switch (args.key) {
|
|
163
|
+
case 'code-font-size':
|
|
164
|
+
return trans.__('Decrease Code Font Size');
|
|
165
|
+
case 'content-font-size1':
|
|
166
|
+
return trans.__('Decrease Content Font Size');
|
|
167
|
+
case 'ui-font-size1':
|
|
168
|
+
return trans.__('Decrease UI Font Size');
|
|
169
|
+
default:
|
|
170
|
+
return trans.__('Decrease Font Size');
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
execute: args => manager.decrFontSize(args['key'] as string)
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return manager;
|
|
177
|
+
},
|
|
178
|
+
autoStart: true,
|
|
179
|
+
provides: IThemeManager
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The default theme manager's UI command palette and main menu functionality.
|
|
184
|
+
*
|
|
185
|
+
* #### Notes
|
|
186
|
+
* This plugin loads separately from the theme manager plugin in order to
|
|
187
|
+
* prevent blocking of the theme manager while it waits for the command palette
|
|
188
|
+
* and main menu to become available.
|
|
189
|
+
*/
|
|
190
|
+
export const themesPaletteMenuPlugin: JupyterFrontEndPlugin<void> = {
|
|
191
|
+
id: '@jupyterlab/apputils-extension:themes-palette-menu',
|
|
192
|
+
requires: [IThemeManager, ITranslator],
|
|
193
|
+
optional: [ICommandPalette, IMainMenu],
|
|
194
|
+
activate: (
|
|
195
|
+
app: JupyterFrontEnd,
|
|
196
|
+
manager: IThemeManager,
|
|
197
|
+
translator: ITranslator,
|
|
198
|
+
palette: ICommandPalette | null,
|
|
199
|
+
mainMenu: IMainMenu | null
|
|
200
|
+
): void => {
|
|
201
|
+
const trans = translator.load('jupyterlab');
|
|
202
|
+
|
|
203
|
+
// If we have a main menu, add the theme manager to the settings menu.
|
|
204
|
+
if (mainMenu) {
|
|
205
|
+
void app.restored.then(() => {
|
|
206
|
+
const isPalette = false;
|
|
207
|
+
|
|
208
|
+
const themeMenu = mainMenu.settingsMenu.items.find(
|
|
209
|
+
item =>
|
|
210
|
+
item.type === 'submenu' &&
|
|
211
|
+
item.submenu?.id === 'jp-mainmenu-settings-apputilstheme'
|
|
212
|
+
)?.submenu;
|
|
213
|
+
|
|
214
|
+
// choose a theme
|
|
215
|
+
if (themeMenu) {
|
|
216
|
+
manager.themes.forEach((theme, index) => {
|
|
217
|
+
themeMenu.insertItem(index, {
|
|
218
|
+
command: CommandIDs.changeTheme,
|
|
219
|
+
args: { isPalette, theme }
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If we have a command palette, add theme switching options to it.
|
|
227
|
+
if (palette) {
|
|
228
|
+
void app.restored.then(() => {
|
|
229
|
+
const category = trans.__('Theme');
|
|
230
|
+
const command = CommandIDs.changeTheme;
|
|
231
|
+
const isPalette = true;
|
|
232
|
+
|
|
233
|
+
// choose a theme
|
|
234
|
+
manager.themes.forEach(theme => {
|
|
235
|
+
palette.addItem({ command, args: { isPalette, theme }, category });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// toggle scrollbar theming
|
|
239
|
+
palette.addItem({ command: CommandIDs.themeScrollbars, category });
|
|
240
|
+
|
|
241
|
+
// increase/decrease code font size
|
|
242
|
+
palette.addItem({
|
|
243
|
+
command: CommandIDs.incrFontSize,
|
|
244
|
+
args: {
|
|
245
|
+
key: 'code-font-size'
|
|
246
|
+
},
|
|
247
|
+
category
|
|
248
|
+
});
|
|
249
|
+
palette.addItem({
|
|
250
|
+
command: CommandIDs.decrFontSize,
|
|
251
|
+
args: {
|
|
252
|
+
key: 'code-font-size'
|
|
253
|
+
},
|
|
254
|
+
category
|
|
255
|
+
});
|
|
256
|
+
// increase/decrease content font size
|
|
257
|
+
palette.addItem({
|
|
258
|
+
command: CommandIDs.incrFontSize,
|
|
259
|
+
args: {
|
|
260
|
+
key: 'content-font-size1'
|
|
261
|
+
},
|
|
262
|
+
category
|
|
263
|
+
});
|
|
264
|
+
palette.addItem({
|
|
265
|
+
command: CommandIDs.decrFontSize,
|
|
266
|
+
args: {
|
|
267
|
+
key: 'content-font-size1'
|
|
268
|
+
},
|
|
269
|
+
category
|
|
270
|
+
});
|
|
271
|
+
// increase/decrease ui font size
|
|
272
|
+
palette.addItem({
|
|
273
|
+
command: CommandIDs.incrFontSize,
|
|
274
|
+
args: {
|
|
275
|
+
key: 'ui-font-size1'
|
|
276
|
+
},
|
|
277
|
+
category
|
|
278
|
+
});
|
|
279
|
+
palette.addItem({
|
|
280
|
+
command: CommandIDs.decrFontSize,
|
|
281
|
+
args: {
|
|
282
|
+
key: 'ui-font-size1'
|
|
283
|
+
},
|
|
284
|
+
category
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
autoStart: true
|
|
290
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
JupyterFrontEnd,
|
|
8
|
+
JupyterFrontEndPlugin
|
|
9
|
+
} from '@jupyterlab/application';
|
|
10
|
+
import {
|
|
11
|
+
createDefaultFactory,
|
|
12
|
+
IToolbarWidgetRegistry,
|
|
13
|
+
ToolbarWidgetRegistry
|
|
14
|
+
} from '@jupyterlab/apputils';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The default toolbar registry.
|
|
18
|
+
*/
|
|
19
|
+
export const toolbarRegistry: JupyterFrontEndPlugin<IToolbarWidgetRegistry> = {
|
|
20
|
+
id: '@jupyterlab/apputils-extension:toolbar-registry',
|
|
21
|
+
autoStart: true,
|
|
22
|
+
provides: IToolbarWidgetRegistry,
|
|
23
|
+
activate: (app: JupyterFrontEnd) => {
|
|
24
|
+
const registry = new ToolbarWidgetRegistry({
|
|
25
|
+
defaultFactory: createDefaultFactory(app.commands)
|
|
26
|
+
});
|
|
27
|
+
return registry;
|
|
28
|
+
}
|
|
29
|
+
};
|
package/src/typings.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
IRouter,
|
|
6
|
+
JupyterFrontEnd,
|
|
7
|
+
JupyterFrontEndPlugin
|
|
8
|
+
} from '@jupyterlab/application';
|
|
9
|
+
import { Dialog, IWindowResolver, showDialog } from '@jupyterlab/apputils';
|
|
10
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
11
|
+
import {
|
|
12
|
+
ABCWidgetFactory,
|
|
13
|
+
DocumentRegistry,
|
|
14
|
+
DocumentWidget,
|
|
15
|
+
IDocumentWidget
|
|
16
|
+
} from '@jupyterlab/docregistry';
|
|
17
|
+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
18
|
+
import { Contents, Workspace, WorkspaceManager } from '@jupyterlab/services';
|
|
19
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
20
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
21
|
+
import { Widget } from '@lumino/widgets';
|
|
22
|
+
|
|
23
|
+
namespace CommandIDs {
|
|
24
|
+
export const saveWorkspace = 'workspace-ui:save';
|
|
25
|
+
|
|
26
|
+
export const saveWorkspaceAs = 'workspace-ui:save-as';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const WORKSPACE_NAME = 'jupyterlab-workspace';
|
|
30
|
+
const WORKSPACE_EXT = '.' + WORKSPACE_NAME;
|
|
31
|
+
const LAST_SAVE_ID = 'workspace-ui:lastSave';
|
|
32
|
+
const ICON_NAME = 'jp-JupyterIcon';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The workspace MIME renderer and save plugin.
|
|
36
|
+
*/
|
|
37
|
+
export const workspacesPlugin: JupyterFrontEndPlugin<void> = {
|
|
38
|
+
id: '@jupyterlab/apputils-extension:workspaces',
|
|
39
|
+
autoStart: true,
|
|
40
|
+
requires: [
|
|
41
|
+
IDefaultFileBrowser,
|
|
42
|
+
IWindowResolver,
|
|
43
|
+
IStateDB,
|
|
44
|
+
ITranslator,
|
|
45
|
+
JupyterFrontEnd.IPaths
|
|
46
|
+
],
|
|
47
|
+
optional: [IRouter],
|
|
48
|
+
activate: (
|
|
49
|
+
app: JupyterFrontEnd,
|
|
50
|
+
fileBrowser: IDefaultFileBrowser,
|
|
51
|
+
resolver: IWindowResolver,
|
|
52
|
+
state: IStateDB,
|
|
53
|
+
translator: ITranslator,
|
|
54
|
+
paths: JupyterFrontEnd.IPaths,
|
|
55
|
+
router: IRouter | null
|
|
56
|
+
): void => {
|
|
57
|
+
// The workspace factory creates dummy widgets to load a new workspace.
|
|
58
|
+
const factory = new Private.WorkspaceFactory({
|
|
59
|
+
workspaces: app.serviceManager.workspaces,
|
|
60
|
+
router,
|
|
61
|
+
state,
|
|
62
|
+
translator,
|
|
63
|
+
paths
|
|
64
|
+
});
|
|
65
|
+
const trans = translator.load('jupyterlab');
|
|
66
|
+
|
|
67
|
+
app.docRegistry.addFileType({
|
|
68
|
+
name: WORKSPACE_NAME,
|
|
69
|
+
contentType: 'file',
|
|
70
|
+
fileFormat: 'text',
|
|
71
|
+
displayName: trans.__('JupyterLab workspace File'),
|
|
72
|
+
extensions: [WORKSPACE_EXT],
|
|
73
|
+
mimeTypes: ['text/json'],
|
|
74
|
+
iconClass: ICON_NAME
|
|
75
|
+
});
|
|
76
|
+
app.docRegistry.addWidgetFactory(factory);
|
|
77
|
+
app.commands.addCommand(CommandIDs.saveWorkspaceAs, {
|
|
78
|
+
label: trans.__('Save Current Workspace As…'),
|
|
79
|
+
execute: async () => {
|
|
80
|
+
const data = app.serviceManager.workspaces.fetch(resolver.name);
|
|
81
|
+
await Private.saveAs(
|
|
82
|
+
fileBrowser,
|
|
83
|
+
app.serviceManager.contents,
|
|
84
|
+
data,
|
|
85
|
+
state,
|
|
86
|
+
translator
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
app.commands.addCommand(CommandIDs.saveWorkspace, {
|
|
92
|
+
label: trans.__('Save Current Workspace'),
|
|
93
|
+
execute: async () => {
|
|
94
|
+
const { contents } = app.serviceManager;
|
|
95
|
+
const data = app.serviceManager.workspaces.fetch(resolver.name);
|
|
96
|
+
const lastSave = (await state.fetch(LAST_SAVE_ID)) as string;
|
|
97
|
+
if (lastSave === undefined) {
|
|
98
|
+
await Private.saveAs(fileBrowser, contents, data, state, translator);
|
|
99
|
+
} else {
|
|
100
|
+
await Private.save(lastSave, contents, data, state);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
namespace Private {
|
|
108
|
+
/**
|
|
109
|
+
* Save workspace to a user provided location
|
|
110
|
+
*/
|
|
111
|
+
export async function save(
|
|
112
|
+
userPath: string,
|
|
113
|
+
contents: Contents.IManager,
|
|
114
|
+
data: Promise<Workspace.IWorkspace>,
|
|
115
|
+
state: IStateDB
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
let name = userPath.split('/').pop();
|
|
118
|
+
|
|
119
|
+
// Add extension if not provided or remove extension from name if it was.
|
|
120
|
+
if (name !== undefined && name.includes('.')) {
|
|
121
|
+
name = name.split('.')[0];
|
|
122
|
+
} else {
|
|
123
|
+
userPath = userPath + WORKSPACE_EXT;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Save last save location, for save button to work
|
|
127
|
+
await state.save(LAST_SAVE_ID, userPath);
|
|
128
|
+
|
|
129
|
+
const resolvedData = await data;
|
|
130
|
+
resolvedData.metadata.id = `${name}`;
|
|
131
|
+
await contents.save(userPath, {
|
|
132
|
+
type: 'file',
|
|
133
|
+
format: 'text',
|
|
134
|
+
content: JSON.stringify(resolvedData)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Ask user for location, and save workspace.
|
|
140
|
+
* Default location is the current directory in the file browser
|
|
141
|
+
*/
|
|
142
|
+
export async function saveAs(
|
|
143
|
+
browser: IDefaultFileBrowser,
|
|
144
|
+
contents: Contents.IManager,
|
|
145
|
+
data: Promise<Workspace.IWorkspace>,
|
|
146
|
+
state: IStateDB,
|
|
147
|
+
translator?: ITranslator
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
translator = translator || nullTranslator;
|
|
150
|
+
const lastSave = await state.fetch(LAST_SAVE_ID);
|
|
151
|
+
|
|
152
|
+
let defaultName;
|
|
153
|
+
if (lastSave === undefined) {
|
|
154
|
+
defaultName = 'new-workspace';
|
|
155
|
+
} else {
|
|
156
|
+
defaultName = (lastSave as string).split('/').pop()?.split('.')[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const defaultPath = browser.model.path + '/' + defaultName + WORKSPACE_EXT;
|
|
160
|
+
const userPath = await getSavePath(defaultPath, translator);
|
|
161
|
+
|
|
162
|
+
if (userPath) {
|
|
163
|
+
await save(userPath, contents, data, state);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* This widget factory is used to handle double click on workspace
|
|
169
|
+
*/
|
|
170
|
+
export class WorkspaceFactory extends ABCWidgetFactory<IDocumentWidget> {
|
|
171
|
+
/**
|
|
172
|
+
* Construct a widget factory that uploads a workspace and navigates to it.
|
|
173
|
+
*
|
|
174
|
+
* @param options - The instantiation options for a `WorkspaceFactory`.
|
|
175
|
+
*/
|
|
176
|
+
constructor(options: WorkspaceFactory.IOptions) {
|
|
177
|
+
const trans = (options.translator || nullTranslator).load('jupyterlab');
|
|
178
|
+
super({
|
|
179
|
+
name: 'Workspace loader',
|
|
180
|
+
label: trans.__('Workspace loader'),
|
|
181
|
+
fileTypes: [WORKSPACE_NAME],
|
|
182
|
+
defaultFor: [WORKSPACE_NAME],
|
|
183
|
+
readOnly: true
|
|
184
|
+
});
|
|
185
|
+
this._application = options.paths.urls.app;
|
|
186
|
+
this._router = options.router;
|
|
187
|
+
this._state = options.state;
|
|
188
|
+
this._workspaces = options.workspaces;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Loads the workspace into load, and jump to it
|
|
193
|
+
* @param context This is used queried to query the workspace content
|
|
194
|
+
*/
|
|
195
|
+
protected createNewWidget(
|
|
196
|
+
context: DocumentRegistry.Context
|
|
197
|
+
): IDocumentWidget {
|
|
198
|
+
// Save a file's contents as a workspace and navigate to that workspace.
|
|
199
|
+
void context.ready.then(async () => {
|
|
200
|
+
const file = context.model;
|
|
201
|
+
const workspace = file.toJSON() as unknown as Workspace.IWorkspace;
|
|
202
|
+
const path = context.path;
|
|
203
|
+
const id = workspace.metadata.id;
|
|
204
|
+
|
|
205
|
+
// Save the file contents as a workspace.
|
|
206
|
+
await this._workspaces.save(id, workspace);
|
|
207
|
+
|
|
208
|
+
// Save last save location for the save command.
|
|
209
|
+
await this._state.save(LAST_SAVE_ID, path);
|
|
210
|
+
|
|
211
|
+
// Navigate to new workspace.
|
|
212
|
+
const url = URLExt.join(this._application, 'workspaces', id);
|
|
213
|
+
if (this._router) {
|
|
214
|
+
this._router.navigate(url, { hard: true });
|
|
215
|
+
} else {
|
|
216
|
+
document.location.href = url;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return dummyWidget(context);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _application: string;
|
|
223
|
+
private _router: IRouter | null;
|
|
224
|
+
private _state: IStateDB;
|
|
225
|
+
private _workspaces: WorkspaceManager;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A namespace for `WorkspaceFactory`
|
|
230
|
+
*/
|
|
231
|
+
export namespace WorkspaceFactory {
|
|
232
|
+
/**
|
|
233
|
+
* Instantiation options for a `WorkspaceFactory`
|
|
234
|
+
*/
|
|
235
|
+
export interface IOptions {
|
|
236
|
+
paths: JupyterFrontEnd.IPaths;
|
|
237
|
+
router: IRouter | null;
|
|
238
|
+
state: IStateDB;
|
|
239
|
+
translator: ITranslator;
|
|
240
|
+
workspaces: WorkspaceManager;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returns a dummy widget with disposed content that doesn't render in the UI.
|
|
246
|
+
*
|
|
247
|
+
* @param context - The file context.
|
|
248
|
+
*/
|
|
249
|
+
function dummyWidget(context: DocumentRegistry.Context): IDocumentWidget {
|
|
250
|
+
const widget = new DocumentWidget({ content: new Widget(), context });
|
|
251
|
+
widget.content.dispose();
|
|
252
|
+
return widget;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Ask user for a path to save to.
|
|
257
|
+
* @param defaultPath Path already present when the dialog is shown
|
|
258
|
+
*/
|
|
259
|
+
async function getSavePath(
|
|
260
|
+
defaultPath: string,
|
|
261
|
+
translator?: ITranslator
|
|
262
|
+
): Promise<string | null> {
|
|
263
|
+
translator = translator || nullTranslator;
|
|
264
|
+
const trans = translator.load('jupyterlab');
|
|
265
|
+
const saveBtn = Dialog.okButton({ label: trans.__('Save') });
|
|
266
|
+
const result = await showDialog({
|
|
267
|
+
title: trans.__('Save Current Workspace As…'),
|
|
268
|
+
body: new SaveWidget(defaultPath),
|
|
269
|
+
buttons: [Dialog.cancelButton(), saveBtn]
|
|
270
|
+
});
|
|
271
|
+
if (result.button.label === trans.__('Save')) {
|
|
272
|
+
return result.value;
|
|
273
|
+
} else {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A widget that gets a file path from a user.
|
|
280
|
+
*/
|
|
281
|
+
class SaveWidget extends Widget {
|
|
282
|
+
/**
|
|
283
|
+
* Gets a modal node for getting save location. Will have a default to the current opened directory
|
|
284
|
+
* @param path Default location
|
|
285
|
+
*/
|
|
286
|
+
constructor(path: string) {
|
|
287
|
+
super({ node: createSaveNode(path) });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Gets the save path entered by the user
|
|
292
|
+
*/
|
|
293
|
+
getValue(): string {
|
|
294
|
+
return (this.node as HTMLInputElement).value;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create the node for a save widget.
|
|
300
|
+
*/
|
|
301
|
+
function createSaveNode(path: string): HTMLElement {
|
|
302
|
+
const input = document.createElement('input');
|
|
303
|
+
input.value = path;
|
|
304
|
+
return input;
|
|
305
|
+
}
|
|
306
|
+
}
|
package/style/base.css
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--jp-private-shortcuts-key-padding-horizontal: 0.47em;
|
|
8
|
+
--jp-private-shortcuts-key-padding-vertical: 0.28em;
|
|
9
|
+
--jp-private-shortcuts-label-padding-horizontal: 0.47em;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.jp-ContextualShortcut-TableRow {
|
|
13
|
+
font-size: var(--jp-ui-font-size1);
|
|
14
|
+
font-family: var(--jp-ui-font-family);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.jp-ContextualShortcut-TableItem {
|
|
18
|
+
margin-left: auto;
|
|
19
|
+
margin-right: auto;
|
|
20
|
+
color: var(--jp-inverse-layout-color0);
|
|
21
|
+
font-size: var(--jp-ui-font-size1);
|
|
22
|
+
line-height: 2em;
|
|
23
|
+
padding-right: var(--jp-private-shortcuts-label-padding-horizontal);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.jp-ContextualShortcut-TableLastRow {
|
|
27
|
+
height: 2em;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.jp-ContextualShortcut-Key {
|
|
31
|
+
font-family: var(--jp-code-font-family);
|
|
32
|
+
border-width: var(--jp-border-width);
|
|
33
|
+
border-radius: var(--jp-border-radius);
|
|
34
|
+
border-style: solid;
|
|
35
|
+
border-color: var(--jp-border-color1);
|
|
36
|
+
color: var(--jp-ui-font-color1);
|
|
37
|
+
background: var(--jp-layout-color1);
|
|
38
|
+
padding-left: var(--jp-private-shortcuts-key-padding-horizontal);
|
|
39
|
+
padding-right: var(--jp-private-shortcuts-key-padding-horizontal);
|
|
40
|
+
padding-top: var(--jp-private-shortcuts-key-padding-vertical);
|
|
41
|
+
padding-bottom: var(--jp-private-shortcuts-key-padding-vertical);
|
|
42
|
+
}
|