xtralab 0.14.0 → 0.15.0

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 (52) hide show
  1. package/lib/customPanel/index.d.ts +7 -0
  2. package/lib/customPanel/index.js +74 -0
  3. package/lib/fileBrowser/commands.js +13 -1
  4. package/lib/fileBrowser/contents.d.ts +11 -0
  5. package/lib/fileBrowser/contents.js +28 -0
  6. package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
  7. package/lib/fileBrowser/dragAndDrop.js +115 -0
  8. package/lib/fileBrowser/fileBrowser.d.ts +2 -0
  9. package/lib/fileBrowser/fileBrowser.js +151 -197
  10. package/lib/fileBrowser/index.js +39 -6
  11. package/lib/fileBrowser/widget.d.ts +44 -5
  12. package/lib/fileBrowser/widget.js +153 -14
  13. package/lib/index.js +4 -0
  14. package/lib/launcher/agents.d.ts +1 -1
  15. package/lib/launcher/agents.js +15 -2
  16. package/lib/launcher/icons.js +15 -4
  17. package/lib/launcher/schemaDefaults.d.ts +2 -9
  18. package/lib/launcher/schemaDefaults.js +37 -13
  19. package/lib/menuBar/icons.d.ts +6 -0
  20. package/lib/menuBar/icons.js +13 -0
  21. package/lib/menuBar/index.d.ts +15 -0
  22. package/lib/menuBar/index.js +164 -0
  23. package/lib/sidebar/index.d.ts +7 -13
  24. package/lib/sidebar/index.js +80 -46
  25. package/lib/terminals/index.d.ts +5 -3
  26. package/lib/terminals/index.js +17 -6
  27. package/lib/terminals/widget.d.ts +29 -25
  28. package/lib/terminals/widget.js +150 -65
  29. package/package.json +1 -1
  30. package/schema/launcher.json +2 -2
  31. package/schema/menu-bar.json +37 -0
  32. package/schema/sidebar.json +47 -7
  33. package/src/customPanel/index.ts +104 -0
  34. package/src/fileBrowser/commands.ts +14 -1
  35. package/src/fileBrowser/contents.ts +30 -0
  36. package/src/fileBrowser/dragAndDrop.ts +178 -0
  37. package/src/fileBrowser/fileBrowser.tsx +183 -225
  38. package/src/fileBrowser/index.ts +41 -6
  39. package/src/fileBrowser/widget.tsx +195 -14
  40. package/src/index.ts +4 -0
  41. package/src/launcher/agents.ts +16 -3
  42. package/src/launcher/icons.ts +16 -4
  43. package/src/launcher/schemaDefaults.ts +43 -13
  44. package/src/menuBar/icons.ts +14 -0
  45. package/src/menuBar/index.ts +195 -0
  46. package/src/sidebar/index.ts +95 -52
  47. package/src/terminals/index.ts +24 -6
  48. package/src/terminals/widget.tsx +186 -87
  49. package/style/base.css +22 -34
  50. package/style/customPanel.css +9 -0
  51. package/style/index.js +1 -0
  52. package/style/terminals.css +19 -77
@@ -0,0 +1,7 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Registers "Custom Panel", which takes a sidebar tab only while it hosts a
4
+ * section.
5
+ */
6
+ declare const plugin: JupyterFrontEndPlugin<void>;
7
+ export default plugin;
@@ -0,0 +1,74 @@
1
+ import { IMovableSectionRegistry } from '@jupyterlab/apputils';
2
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
3
+ import { SidePanel, tableRowsIcon } from '@jupyterlab/ui-components';
4
+ import { Signal } from '@lumino/signaling';
5
+ const PLUGIN_ID = 'xtralab:custom-panel';
6
+ const CUSTOM_PANEL_ID = 'xtralab-custom-panel';
7
+ /**
8
+ * A sidebar panel holding only the sections moved in from other panels.
9
+ */
10
+ class CustomPanel extends SidePanel {
11
+ constructor(options) {
12
+ super();
13
+ this._sectionCountChanged = new Signal(this);
14
+ this.id = CUSTOM_PANEL_ID;
15
+ this.title.icon = tableRowsIcon;
16
+ this.title.caption = options.trans.__('Custom Panel');
17
+ this.addClass('jp-xtralab-CustomPanel');
18
+ }
19
+ get accordionPanel() {
20
+ return this.content;
21
+ }
22
+ get sections() {
23
+ return this.content.widgets;
24
+ }
25
+ get sectionCountChanged() {
26
+ return this._sectionCountChanged;
27
+ }
28
+ addSection(widget) {
29
+ this.addWidget(widget);
30
+ this._sectionCountChanged.emit(this.content.widgets.length);
31
+ }
32
+ removeSectionWidget(widget) {
33
+ if (widget.parent !== this.content) {
34
+ return;
35
+ }
36
+ widget.parent = null;
37
+ this._sectionCountChanged.emit(this.content.widgets.length);
38
+ }
39
+ }
40
+ /**
41
+ * Registers "Custom Panel", which takes a sidebar tab only while it hosts a
42
+ * section.
43
+ */
44
+ const plugin = {
45
+ id: PLUGIN_ID,
46
+ description: 'A user-assembled sidebar panel hosting sections moved in from other panels; hidden while empty.',
47
+ autoStart: true,
48
+ optional: [IMovableSectionRegistry, ITranslator],
49
+ activate: (app, registry, translator) => {
50
+ if (!registry) {
51
+ return;
52
+ }
53
+ const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
54
+ const panel = new CustomPanel({ trans });
55
+ // Only a user move should switch the sidebar, not the startup restoration.
56
+ let restored = false;
57
+ void app.restored.then(() => {
58
+ restored = true;
59
+ });
60
+ panel.sectionCountChanged.connect((_, count) => {
61
+ if (count > 0 && !panel.parent) {
62
+ app.shell.add(panel, 'left', { rank: 3 });
63
+ if (restored) {
64
+ app.shell.activateById(panel.id);
65
+ }
66
+ }
67
+ else if (count === 0 && panel.parent) {
68
+ panel.parent = null;
69
+ }
70
+ });
71
+ registry.registerTarget(PLUGIN_ID, trans.__('Custom Panel'), panel);
72
+ }
73
+ };
74
+ export default plugin;
@@ -418,12 +418,24 @@ export function registerCommands(opts) {
418
418
  // point of view. An empty path is the "workspace root" gesture —
419
419
  // there is no tree row for the root itself, so the browser is asked
420
420
  // to scroll back to the top and clear its selection instead.
421
+ // The tree is movable, so surface whichever sidebar widget holds it.
422
+ const activateTreeHost = () => {
423
+ for (const area of ['left', 'right']) {
424
+ for (const widget of app.shell.widgets(area)) {
425
+ if (widget.node.contains(browser.sectionNode)) {
426
+ app.shell.activateById(widget.id);
427
+ return;
428
+ }
429
+ }
430
+ }
431
+ app.shell.activateById(FILE_BROWSER_ID);
432
+ };
421
433
  commands.addCommand(CommandIDs.revealPath, {
422
434
  label: trans.__('Reveal in File Browser'),
423
435
  execute: (args) => {
424
436
  var _a;
425
437
  const path = (_a = args.path) !== null && _a !== void 0 ? _a : '';
426
- app.shell.activateById(FILE_BROWSER_ID);
438
+ activateTreeHost();
427
439
  if (path.length === 0) {
428
440
  browser.scrollToRoot();
429
441
  }
@@ -10,6 +10,17 @@ export declare const ROOT_LOAD_KEY = "";
10
10
  * still be represented in the flat path list.
11
11
  */
12
12
  export declare function toCanonicalPath(child: Contents.IModel): string;
13
+ /**
14
+ * Compute the canonical parent path for a canonical path. Returns
15
+ * {@link ROOT_LOAD_KEY} for top-level entries.
16
+ */
17
+ export declare function parentOf(canonicalPath: string): string;
18
+ /**
19
+ * The last segment of a canonical path. Directories keep their trailing
20
+ * slash, so appending the result to a canonical directory path yields a
21
+ * canonical path again.
22
+ */
23
+ export declare function canonicalBasename(canonicalPath: string): string;
13
24
  /**
14
25
  * Strip the trailing slash that `@pierre/trees` uses for canonical directory
15
26
  * paths so the value can be sent to the Jupyter contents API.
@@ -12,6 +12,34 @@ export const ROOT_LOAD_KEY = '';
12
12
  export function toCanonicalPath(child) {
13
13
  return child.type === 'directory' ? `${child.path}${DIR_SUFFIX}` : child.path;
14
14
  }
15
+ /**
16
+ * Compute the canonical parent path for a canonical path. Returns
17
+ * {@link ROOT_LOAD_KEY} for top-level entries.
18
+ */
19
+ export function parentOf(canonicalPath) {
20
+ const trimmed = canonicalPath.endsWith(DIR_SUFFIX)
21
+ ? canonicalPath.slice(0, -DIR_SUFFIX.length)
22
+ : canonicalPath;
23
+ const idx = trimmed.lastIndexOf(DIR_SUFFIX);
24
+ if (idx < 0) {
25
+ return ROOT_LOAD_KEY;
26
+ }
27
+ return `${trimmed.slice(0, idx)}${DIR_SUFFIX}`;
28
+ }
29
+ /**
30
+ * The last segment of a canonical path. Directories keep their trailing
31
+ * slash, so appending the result to a canonical directory path yields a
32
+ * canonical path again.
33
+ */
34
+ export function canonicalBasename(canonicalPath) {
35
+ const isDir = canonicalPath.endsWith(DIR_SUFFIX);
36
+ const trimmed = isDir
37
+ ? canonicalPath.slice(0, -DIR_SUFFIX.length)
38
+ : canonicalPath;
39
+ const idx = trimmed.lastIndexOf(DIR_SUFFIX);
40
+ const base = idx < 0 ? trimmed : trimmed.slice(idx + DIR_SUFFIX.length);
41
+ return isDir ? `${base}${DIR_SUFFIX}` : base;
42
+ }
15
43
  /**
16
44
  * Strip the trailing slash that `@pierre/trees` uses for canonical directory
17
45
  * paths so the value can be sent to the Jupyter contents API.
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import type { FileTree, FileTreeDragAndDropConfig, FileTreeDropContext, FileTreeDropResult } from '@pierre/trees';
3
+ /**
4
+ * One tree-model move a drop performs, as canonical paths.
5
+ */
6
+ export interface IDropMove {
7
+ from: string;
8
+ to: string;
9
+ }
10
+ /**
11
+ * Receives drop outcomes from the `@pierre/trees` drag-and-drop
12
+ * machinery. Implemented inside the file browser's contents-sync
13
+ * effect, which owns the load-state and contents-API bookkeeping.
14
+ */
15
+ export interface ITreeDropHandler {
16
+ /**
17
+ * The tree applied the drop to its model: persist the move and
18
+ * re-key the load-state mirrors.
19
+ */
20
+ completeDrop(result: FileTreeDropResult): void;
21
+ /**
22
+ * The tree refused the drop (typically a name collision) and left
23
+ * its model untouched.
24
+ */
25
+ failedDrop(error: string, context: FileTreeDropContext): void;
26
+ /**
27
+ * A drop on the empty space below the last row: move the dragged
28
+ * paths to the workspace root.
29
+ */
30
+ dropOnRoot(draggedPaths: readonly string[]): void;
31
+ }
32
+ /**
33
+ * The from→to moves a drop performs: each dragged path keeps its
34
+ * basename under the target directory. Paths already directly inside
35
+ * the target are skipped.
36
+ */
37
+ export declare function computeDropMoves(context: FileTreeDropContext): IDropMove[];
38
+ /**
39
+ * The moves for a drop of `paths` onto the workspace root.
40
+ */
41
+ export declare function computeRootDropMoves(paths: readonly string[]): IDropMove[];
42
+ /**
43
+ * Build the `dragAndDrop` option for `useFileTree`. The tree captures
44
+ * its options once, so the callbacks delegate through `handlerRef`,
45
+ * filled by the contents-sync effect. `canDrop` hides the drop
46
+ * highlight for all-no-op drops, which the library would accept.
47
+ */
48
+ export declare function createTreeDragAndDropConfig(handlerRef: React.RefObject<ITreeDropHandler | null>): FileTreeDragAndDropConfig;
49
+ interface IRootDropZoneOptions {
50
+ model: FileTree;
51
+ handlerRef: React.RefObject<ITreeDropHandler | null>;
52
+ /**
53
+ * The light-DOM wrapper around the tree host; composed drag events
54
+ * bubble out of the shadow root to it.
55
+ */
56
+ wrapperRef: React.RefObject<HTMLDivElement | null>;
57
+ }
58
+ /**
59
+ * Accept drops on the empty space below the last row as moves to the
60
+ * workspace root — the tree resolves a drop target only while the
61
+ * cursor is over a row and silently discards such drops. dragstart
62
+ * records the dragged paths; a drop whose composed path contains no
63
+ * row hands them over as a root move.
64
+ */
65
+ export declare function useRootDropZone(options: IRootDropZoneOptions): void;
66
+ export {};
@@ -0,0 +1,115 @@
1
+ import * as React from 'react';
2
+ import { ROOT_LOAD_KEY, canonicalBasename, parentOf } from './contents';
3
+ /**
4
+ * The from→to moves a drop performs: each dragged path keeps its
5
+ * basename under the target directory. Paths already directly inside
6
+ * the target are skipped.
7
+ */
8
+ export function computeDropMoves(context) {
9
+ var _a;
10
+ const dir = (_a = context.target.directoryPath) !== null && _a !== void 0 ? _a : ROOT_LOAD_KEY;
11
+ return context.draggedPaths
12
+ .filter(path => parentOf(path) !== dir)
13
+ .map(path => ({ from: path, to: `${dir}${canonicalBasename(path)}` }));
14
+ }
15
+ /**
16
+ * The drop target the tree reports for a move to the workspace root.
17
+ */
18
+ const ROOT_DROP_TARGET = {
19
+ directoryPath: null,
20
+ flattenedSegmentPath: null,
21
+ hoveredPath: null,
22
+ kind: 'root'
23
+ };
24
+ /**
25
+ * The moves for a drop of `paths` onto the workspace root.
26
+ */
27
+ export function computeRootDropMoves(paths) {
28
+ return computeDropMoves({ draggedPaths: paths, target: ROOT_DROP_TARGET });
29
+ }
30
+ /**
31
+ * Build the `dragAndDrop` option for `useFileTree`. The tree captures
32
+ * its options once, so the callbacks delegate through `handlerRef`,
33
+ * filled by the contents-sync effect. `canDrop` hides the drop
34
+ * highlight for all-no-op drops, which the library would accept.
35
+ */
36
+ export function createTreeDragAndDropConfig(handlerRef) {
37
+ return {
38
+ canDrop: context => computeDropMoves(context).length > 0,
39
+ onDropComplete: result => {
40
+ var _a;
41
+ (_a = handlerRef.current) === null || _a === void 0 ? void 0 : _a.completeDrop(result);
42
+ },
43
+ onDropError: (error, context) => {
44
+ var _a;
45
+ (_a = handlerRef.current) === null || _a === void 0 ? void 0 : _a.failedDrop(error, context);
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Accept drops on the empty space below the last row as moves to the
51
+ * workspace root — the tree resolves a drop target only while the
52
+ * cursor is over a row and silently discards such drops. dragstart
53
+ * records the dragged paths; a drop whose composed path contains no
54
+ * row hands them over as a root move.
55
+ */
56
+ export function useRootDropZone(options) {
57
+ const { model, handlerRef, wrapperRef } = options;
58
+ React.useEffect(() => {
59
+ const wrapper = wrapperRef.current;
60
+ if (wrapper === null) {
61
+ return;
62
+ }
63
+ let draggedPaths = null;
64
+ const findRow = (event) => {
65
+ for (const target of event.composedPath()) {
66
+ if (target instanceof HTMLElement && target.dataset.type === 'item') {
67
+ return target;
68
+ }
69
+ }
70
+ return null;
71
+ };
72
+ const handleDragStart = (event) => {
73
+ var _a;
74
+ draggedPaths = null;
75
+ // The tree cancels dragstart when it refuses a session, e.g.
76
+ // while the filter is open.
77
+ if (event.defaultPrevented) {
78
+ return;
79
+ }
80
+ const path = (_a = findRow(event)) === null || _a === void 0 ? void 0 : _a.dataset.itemPath;
81
+ if (path === undefined || path.length === 0) {
82
+ return;
83
+ }
84
+ // The selection is the dragged set (the tree selects the pressed
85
+ // row); entries nested under a dragged folder travel with it.
86
+ const selection = model.getSelectedPaths();
87
+ const candidates = selection.includes(path) ? selection : [path];
88
+ const folders = candidates.filter(c => c.endsWith('/'));
89
+ draggedPaths = candidates.filter(c => !folders.some(folder => folder !== c && c.startsWith(folder)));
90
+ };
91
+ const handleDrop = (event) => {
92
+ var _a;
93
+ const paths = draggedPaths;
94
+ draggedPaths = null;
95
+ // Drops on a row were already resolved by the tree itself.
96
+ if (paths === null || findRow(event) !== null) {
97
+ return;
98
+ }
99
+ event.preventDefault();
100
+ (_a = handlerRef.current) === null || _a === void 0 ? void 0 : _a.dropOnRoot(paths);
101
+ };
102
+ const handleDragEnd = () => {
103
+ draggedPaths = null;
104
+ };
105
+ wrapper.addEventListener('dragstart', handleDragStart);
106
+ wrapper.addEventListener('drop', handleDrop);
107
+ window.addEventListener('dragend', handleDragEnd);
108
+ return () => {
109
+ wrapper.removeEventListener('dragstart', handleDragStart);
110
+ wrapper.removeEventListener('drop', handleDrop);
111
+ window.removeEventListener('dragend', handleDragEnd);
112
+ draggedPaths = null;
113
+ };
114
+ }, [model, handlerRef, wrapperRef]);
115
+ }
@@ -1,11 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { IDocumentManager } from '@jupyterlab/docmanager';
3
3
  import { Contents } from '@jupyterlab/services';
4
+ import { ITranslator } from '@jupyterlab/translation';
4
5
  import type { XtralabFileBrowser } from './widget';
5
6
  interface IFileBrowserProps {
6
7
  contentsManager: Contents.IManager;
7
8
  docManager: IDocumentManager;
8
9
  onOpenFile?: (serverPath: string) => void;
10
+ translator?: ITranslator;
9
11
  /**
10
12
  * The host widget. Selection-change events are pushed up so context-menu
11
13
  * commands can react to what the user has selected.