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
@@ -1,28 +1,14 @@
1
1
  import * as React from 'react';
2
- import { fileIcon } from '@jupyterlab/ui-components';
3
- import { MimeData, PromiseDelegate } from '@lumino/coreutils';
4
- import { Drag } from '@lumino/dragdrop';
2
+ import { showErrorMessage } from '@jupyterlab/apputils';
3
+ import { renameFile } from '@jupyterlab/docmanager';
4
+ import { nullTranslator } from '@jupyterlab/translation';
5
5
  import { Poll } from '@lumino/polling';
6
6
  import { FileTree, useFileTree } from '@pierre/trees/react';
7
- import { ROOT_LOAD_KEY, listDirectory, toServerPath } from './contents';
7
+ import { ROOT_LOAD_KEY, listDirectory, parentOf, toServerPath } from './contents';
8
+ import { computeDropMoves, computeRootDropMoves, createTreeDragAndDropConfig, useRootDropZone } from './dragAndDrop';
8
9
  import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
9
10
  import { loadGitStatusEntries } from './gitStatus';
10
11
  import { FILE_BROWSER_ICONS } from './icons';
11
- /**
12
- * MIME types used by the dock panel and other JupyterLab drop targets.
13
- * `FACTORY_MIME` is the lumino dock-panel contract: the value must be a
14
- * synchronous function that returns a Widget. The contents MIME types
15
- * mirror what the default file browser sends so other drop targets that
16
- * understand them (the file browser itself, custom drop zones) keep
17
- * working when the user drags out of xtralab.
18
- */
19
- const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
20
- const CONTENTS_MIME = 'application/x-jupyter-icontents';
21
- /**
22
- * Threshold in pixels before a press-and-drag is treated as a drag rather
23
- * than a click. Matches the value used by the default JupyterLab listing.
24
- */
25
- const DRAG_THRESHOLD = 5;
26
12
  /**
27
13
  * Polling cadence for the git status decoration. Out-of-band changes (a
28
14
  * terminal `git add`, a `git pull`, a file edited outside the JupyterLab
@@ -69,7 +55,10 @@ const FILE_TREE_TAG = 'file-tree-container';
69
55
  * box unless the host carries the visibility marker set by the filter
70
56
  * bridge effect below — `@pierre/trees` always renders the box when
71
57
  * `search` is enabled, and its stylesheet lives in the shadow root where
72
- * outside CSS cannot reach.
58
+ * outside CSS cannot reach. The third rule restyles the drag-hover row:
59
+ * the library paints it with the selection background, which xtralab
60
+ * maps to a full-strength brand color that is illegible without the
61
+ * inverted selection foreground — hence the quieter ring.
73
62
  */
74
63
  const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
75
64
  '[data-item-section="spacing-item"] {' +
@@ -78,6 +67,10 @@ const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
78
67
  ':host(:not([data-xtralab-filter-visible])) ' +
79
68
  '[data-file-tree-search-container] {' +
80
69
  'display: none;' +
70
+ '}' +
71
+ '[data-type="item"][data-item-drag-target="true"] {' +
72
+ 'background-color: var(--trees-bg-muted);' +
73
+ 'box-shadow: inset 0 0 0 2px var(--trees-accent);' +
81
74
  '}';
82
75
  /**
83
76
  * Renders a `@pierre/trees` file tree backed by the Jupyter contents API.
@@ -89,14 +82,17 @@ const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
89
82
  * state map.
90
83
  */
91
84
  export function FileBrowserComponent(props) {
92
- const { contentsManager, docManager, onOpenFile, widget } = props;
85
+ const { contentsManager, docManager, onOpenFile, translator, widget } = props;
86
+ const trans = React.useMemo(() => (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab'), [translator]);
87
+ const dropHandlerRef = React.useRef(null);
93
88
  const { model } = useFileTree({
94
89
  paths: [],
95
90
  initialExpansion: 'closed',
96
91
  search: true,
97
92
  icons: FILE_BROWSER_ICONS,
98
93
  itemHeight: 24,
99
- unsafeCSS: FILE_TREE_UNSAFE_CSS
94
+ unsafeCSS: FILE_TREE_UNSAFE_CSS,
95
+ dragAndDrop: createTreeDragAndDropConfig(dropHandlerRef)
100
96
  });
101
97
  React.useEffect(() => {
102
98
  const knownDirs = new Map();
@@ -218,20 +214,6 @@ export function FileBrowserComponent(props) {
218
214
  knownDirs.set(canonicalPath, 'unloaded');
219
215
  }
220
216
  };
221
- /**
222
- * Compute the canonical parent path for a given canonical path.
223
- * Returns {@link ROOT_LOAD_KEY} for top-level entries.
224
- */
225
- const parentOf = (canonicalPath) => {
226
- const trimmed = canonicalPath.endsWith('/')
227
- ? canonicalPath.slice(0, -1)
228
- : canonicalPath;
229
- const idx = trimmed.lastIndexOf('/');
230
- if (idx < 0) {
231
- return ROOT_LOAD_KEY;
232
- }
233
- return `${trimmed.slice(0, idx)}/`;
234
- };
235
217
  /**
236
218
  * Refresh every directory currently loaded into the tree. Walks down
237
219
  * from the root through the previously-expanded subdirectories so the
@@ -619,6 +601,136 @@ export function FileBrowserComponent(props) {
619
601
  parentDir.expand();
620
602
  }
621
603
  };
604
+ /**
605
+ * Re-key the load-state mirrors after the tree model moved
606
+ * `fromPath` to `toPath`; a moved directory rewrites every entry
607
+ * under its old prefix.
608
+ */
609
+ const rekeyMirrors = (fromPath, toPath) => {
610
+ if (fromPath.endsWith('/')) {
611
+ for (const path of [...loadedPaths]) {
612
+ if (path.startsWith(fromPath)) {
613
+ loadedPaths.delete(path);
614
+ loadedPaths.add(`${toPath}${path.slice(fromPath.length)}`);
615
+ }
616
+ }
617
+ for (const [dir, state] of [...knownDirs.entries()]) {
618
+ if (dir !== ROOT_LOAD_KEY && dir.startsWith(fromPath)) {
619
+ knownDirs.delete(dir);
620
+ knownDirs.set(`${toPath}${dir.slice(fromPath.length)}`, state);
621
+ }
622
+ }
623
+ }
624
+ else if (loadedPaths.delete(fromPath)) {
625
+ loadedPaths.add(toPath);
626
+ }
627
+ };
628
+ /**
629
+ * Rename through the document manager, so collisions surface the
630
+ * standard overwrite dialog and open widgets follow their file.
631
+ * Resolves false when the move did not happen — a declined
632
+ * overwrite (upstream's 'File not renamed' sentinel) or a failure.
633
+ */
634
+ const renameOnServer = async (fromPath, toPath) => {
635
+ try {
636
+ await renameFile(docManager, toServerPath(fromPath), toServerPath(toPath));
637
+ return true;
638
+ }
639
+ catch (err) {
640
+ if (err !== 'File not renamed') {
641
+ await showErrorMessage(trans.__('Move failed'), err);
642
+ }
643
+ return false;
644
+ }
645
+ };
646
+ /**
647
+ * Persist moves the tree model already applied. A move that did
648
+ * not reach the disk leaves the tree out of step, so a full
649
+ * refresh then restores server truth.
650
+ */
651
+ const persistDropMoves = async (moves) => {
652
+ let failed = false;
653
+ for (const move of moves) {
654
+ if (!(await renameOnServer(move.from, move.to))) {
655
+ failed = true;
656
+ }
657
+ if (cancelled) {
658
+ return;
659
+ }
660
+ }
661
+ if (failed) {
662
+ void refreshAll();
663
+ }
664
+ };
665
+ const completeDrop = (result) => {
666
+ if (cancelled) {
667
+ return;
668
+ }
669
+ const moves = computeDropMoves(result);
670
+ for (const move of moves) {
671
+ rekeyMirrors(move.from, move.to);
672
+ }
673
+ syncGitStatus();
674
+ void persistDropMoves(moves);
675
+ };
676
+ /**
677
+ * Server-side renames for moves the tree model did not apply; the
678
+ * `fileChanged` signals nudge the listing poll to reconcile.
679
+ */
680
+ const persistFallbackMoves = async (moves) => {
681
+ for (const move of moves) {
682
+ await renameOnServer(move.from, move.to);
683
+ if (cancelled) {
684
+ return;
685
+ }
686
+ }
687
+ };
688
+ /**
689
+ * The tree refused the drop and left its model untouched — almost
690
+ * always a name collision at the destination.
691
+ */
692
+ const failedDrop = (error, context) => {
693
+ if (cancelled) {
694
+ return;
695
+ }
696
+ const moves = computeDropMoves(context);
697
+ if (moves.length === 0) {
698
+ console.error('xtralab: drop failed', error);
699
+ return;
700
+ }
701
+ void persistFallbackMoves(moves);
702
+ };
703
+ /**
704
+ * Move the dragged paths to the workspace root: apply to the model
705
+ * first so the rows relocate instantly, then persist. A path the
706
+ * model cannot move (name collision) falls back to the server-side
707
+ * rename and its dialog.
708
+ */
709
+ const dropOnRoot = (draggedPaths) => {
710
+ if (cancelled) {
711
+ return;
712
+ }
713
+ const applied = [];
714
+ const fallback = [];
715
+ for (const move of computeRootDropMoves(draggedPaths)) {
716
+ try {
717
+ model.move(move.from, move.to);
718
+ rekeyMirrors(move.from, move.to);
719
+ applied.push(move);
720
+ }
721
+ catch (_a) {
722
+ fallback.push(move);
723
+ }
724
+ }
725
+ if (applied.length > 0) {
726
+ syncGitStatus();
727
+ void persistDropMoves(applied);
728
+ }
729
+ if (fallback.length > 0) {
730
+ void persistFallbackMoves(fallback);
731
+ }
732
+ };
733
+ dropHandlerRef.current = { completeDrop, failedDrop, dropOnRoot };
622
734
  knownDirs.set(ROOT_LOAD_KEY, 'unloaded');
623
735
  void fetchDirectory(ROOT_LOAD_KEY);
624
736
  void refreshGitignoreMatcher();
@@ -718,6 +830,7 @@ export function FileBrowserComponent(props) {
718
830
  }
719
831
  return () => {
720
832
  cancelled = true;
833
+ dropHandlerRef.current = null;
721
834
  gitStatusPoll.dispose();
722
835
  contentsManager.fileChanged.disconnect(onContentsFileChanged);
723
836
  listingPoll.dispose();
@@ -740,7 +853,7 @@ export function FileBrowserComponent(props) {
740
853
  }
741
854
  }
742
855
  };
743
- }, [model, contentsManager, widget]);
856
+ }, [model, contentsManager, widget, docManager, trans]);
744
857
  // Bridge the tree's selection state up to the widget so command handlers
745
858
  // can read it without depending on React internals. The tree exposes its
746
859
  // selection through `getSelectedPaths()` and emits a generic notification
@@ -896,166 +1009,7 @@ export function FileBrowserComponent(props) {
896
1009
  wrapper.removeEventListener('contextmenu', handleContextMenu, true);
897
1010
  };
898
1011
  }, []);
899
- // Drag a file row out of the tree onto the JupyterLab main area. The row
900
- // lives in the `@pierre/trees` shadow DOM, so we listen to mousedown on
901
- // the wrapper and walk `composedPath()` to recover the row. The drag
902
- // payload uses Lumino's `FACTORY_MIME` contract: the dock panel calls the
903
- // factory function on drop and adds the returned widget to its layout.
904
- React.useEffect(() => {
905
- const wrapper = wrapperRef.current;
906
- if (wrapper === null) {
907
- return;
908
- }
909
- let press = null;
910
- let activeDrag = null;
911
- const findRow = (event) => {
912
- for (const target of event.composedPath()) {
913
- if (!(target instanceof HTMLElement)) {
914
- continue;
915
- }
916
- if (target.dataset.type === 'item') {
917
- return target;
918
- }
919
- }
920
- return null;
921
- };
922
- const cleanup = () => {
923
- press = null;
924
- document.removeEventListener('mousemove', handleMouseMove, true);
925
- document.removeEventListener('mouseup', handleMouseUp, true);
926
- };
927
- const handleMouseDown = (event) => {
928
- // Only react to the primary button. Do not interfere with right-click
929
- // (handled by the contextmenu bridge) or middle-click (browser default).
930
- if (event.button !== 0) {
931
- return;
932
- }
933
- if (activeDrag !== null) {
934
- return;
935
- }
936
- const row = findRow(event);
937
- if (row === null || row.dataset.itemType !== 'file') {
938
- return;
939
- }
940
- const itemPath = row.dataset.itemPath;
941
- if (itemPath === undefined || itemPath.length === 0) {
942
- return;
943
- }
944
- press = { x: event.clientX, y: event.clientY, path: itemPath };
945
- document.addEventListener('mousemove', handleMouseMove, true);
946
- document.addEventListener('mouseup', handleMouseUp, true);
947
- };
948
- const handleMouseMove = (event) => {
949
- if (press === null) {
950
- return;
951
- }
952
- const dx = Math.abs(event.clientX - press.x);
953
- const dy = Math.abs(event.clientY - press.y);
954
- if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
955
- return;
956
- }
957
- const startedFrom = press;
958
- cleanup();
959
- startDrag(startedFrom.path, event.clientX, event.clientY);
960
- };
961
- const handleMouseUp = () => {
962
- cleanup();
963
- };
964
- /**
965
- * Build the list of file paths to drag. If the user pressed on a row
966
- * that is part of the current selection, drag every selected file.
967
- * Otherwise, drag just the row that was pressed. Folders never join a
968
- * drag-out — they have no docmanager widget to open.
969
- */
970
- const collectDragPaths = (sourcePath) => {
971
- const selection = model.getSelectedPaths();
972
- const sourceInSelection = selection.includes(sourcePath);
973
- const candidates = sourceInSelection ? selection : [sourcePath];
974
- const fileServerPaths = [];
975
- for (const candidate of candidates) {
976
- if (candidate.endsWith('/')) {
977
- continue;
978
- }
979
- fileServerPaths.push(toServerPath(candidate));
980
- }
981
- // The source row is always a file, so the list cannot be empty.
982
- if (fileServerPaths.length === 0) {
983
- fileServerPaths.push(toServerPath(sourcePath));
984
- }
985
- return fileServerPaths;
986
- };
987
- const startDrag = (sourcePath, clientX, clientY) => {
988
- const paths = collectDragPaths(sourcePath);
989
- const sourceServerPath = toServerPath(sourcePath);
990
- const dragImage = createDragImage(paths.length);
991
- const drag = new Drag({
992
- dragImage,
993
- mimeData: new MimeData(),
994
- supportedActions: 'copy-move',
995
- proposedAction: 'move'
996
- });
997
- drag.mimeData.setData(CONTENTS_MIME, paths);
998
- // The factory is called by the lumino dock panel on drop. It must
999
- // return a Widget synchronously. For multi-file drags we open the
1000
- // remaining files asynchronously after the first one is placed,
1001
- // mirroring the default file browser's behavior.
1002
- const otherPaths = paths.filter(p => p !== sourceServerPath);
1003
- drag.mimeData.setData(FACTORY_MIME, () => {
1004
- let widget = docManager.findWidget(sourceServerPath);
1005
- if (widget === undefined) {
1006
- widget = docManager.open(sourceServerPath);
1007
- }
1008
- if (otherPaths.length > 0) {
1009
- const firstPlaced = new PromiseDelegate();
1010
- void firstPlaced.promise.then(() => {
1011
- let prev = widget;
1012
- for (const otherPath of otherPaths) {
1013
- const opened = docManager.openOrReveal(otherPath, undefined, undefined, prev !== undefined
1014
- ? { ref: prev.id, mode: 'tab-after' }
1015
- : undefined);
1016
- if (opened !== undefined) {
1017
- prev = opened;
1018
- }
1019
- }
1020
- });
1021
- firstPlaced.resolve();
1022
- }
1023
- return widget;
1024
- });
1025
- activeDrag = drag;
1026
- void drag.start(clientX, clientY).then(() => {
1027
- activeDrag = null;
1028
- });
1029
- };
1030
- wrapper.addEventListener('mousedown', handleMouseDown);
1031
- return () => {
1032
- wrapper.removeEventListener('mousedown', handleMouseDown);
1033
- cleanup();
1034
- activeDrag === null || activeDrag === void 0 ? void 0 : activeDrag.dispose();
1035
- activeDrag = null;
1036
- };
1037
- }, [model, docManager]);
1012
+ useRootDropZone({ model, handlerRef: dropHandlerRef, wrapperRef });
1038
1013
  return (React.createElement("div", { ref: wrapperRef, style: { display: 'flex', flex: '1 1 auto', minHeight: 0 } },
1039
1014
  React.createElement(FileTree, { model: model, style: { height: '100%', width: '100%' } })));
1040
1015
  }
1041
- /**
1042
- * Build the small badge shown next to the cursor while dragging. The
1043
- * default file browser renders a richer image with the file's icon, but
1044
- * we don't have a per-file icon at this layer — just the count of files
1045
- * being dragged is enough to give the user feedback.
1046
- */
1047
- function createDragImage(count) {
1048
- const node = document.createElement('div');
1049
- node.className = 'jp-xtralab-DragImage';
1050
- const iconWrapper = document.createElement('span');
1051
- iconWrapper.className = 'jp-xtralab-DragImage-icon';
1052
- fileIcon.element({ container: iconWrapper, stylesheet: 'menuItem' });
1053
- node.appendChild(iconWrapper);
1054
- if (count > 1) {
1055
- const badge = document.createElement('span');
1056
- badge.className = 'jp-xtralab-DragImage-count';
1057
- badge.textContent = String(count);
1058
- node.appendChild(badge);
1059
- }
1060
- return node;
1061
- }
@@ -1,10 +1,10 @@
1
1
  import { ILayoutRestorer } from '@jupyterlab/application';
2
- import { ICommandPalette } from '@jupyterlab/apputils';
2
+ import { ICommandPalette, IMovableSectionRegistry } from '@jupyterlab/apputils';
3
3
  import { IDocumentManager } from '@jupyterlab/docmanager';
4
4
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
5
- import { ITranslator } from '@jupyterlab/translation';
5
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
6
6
  import { populateToolbar, registerCommands } from './commands';
7
- import { XtralabFileBrowser } from './widget';
7
+ import { FILE_BROWSER_ID, XtralabFileBrowser } from './widget';
8
8
  const PLUGIN_ID = 'xtralab:plugin';
9
9
  /**
10
10
  * The file browser plugin. Adds a `@pierre/trees`-powered file browser to the
@@ -15,14 +15,21 @@ const plugin = {
15
15
  description: 'A path-first file browser for JupyterLab built on @pierre/trees.',
16
16
  autoStart: true,
17
17
  requires: [IDocumentManager],
18
- optional: [ILayoutRestorer, ISettingRegistry, ICommandPalette, ITranslator],
19
- activate: (app, docManager, restorer, settingRegistry, palette, translator) => {
18
+ optional: [
19
+ ILayoutRestorer,
20
+ ISettingRegistry,
21
+ ICommandPalette,
22
+ ITranslator,
23
+ IMovableSectionRegistry
24
+ ],
25
+ activate: (app, docManager, restorer, settingRegistry, palette, translator, movableSections) => {
20
26
  const browser = new XtralabFileBrowser({
21
27
  contentsManager: app.serviceManager.contents,
22
28
  docManager,
23
29
  onOpenFile: (serverPath) => {
24
30
  void app.commands.execute('docmanager:open', { path: serverPath });
25
- }
31
+ },
32
+ translator: translator !== null && translator !== void 0 ? translator : undefined
26
33
  });
27
34
  // Ranks just under the Terminals panel in the left sidebar. The shipped
28
35
  // `layout` setting assigns the default ranks (rank only, no area pin),
@@ -34,6 +41,32 @@ const plugin = {
34
41
  }
35
42
  registerCommands({ app, browser, docManager, palette, translator });
36
43
  populateToolbar({ app, browser });
44
+ if (movableSections) {
45
+ const label = (translator !== null && translator !== void 0 ? translator : nullTranslator)
46
+ .load('jupyterlab')
47
+ .__('Files');
48
+ movableSections.registerSource(FILE_BROWSER_ID, label, browser);
49
+ movableSections.registerTarget(FILE_BROWSER_ID, label, browser);
50
+ // Only a user move should switch the sidebar, not the startup restoration.
51
+ let settled = false;
52
+ void app.restored.then(() => {
53
+ settled = true;
54
+ });
55
+ browser.contentChanged.connect(() => {
56
+ if (browser.isEmpty) {
57
+ if (browser.parent) {
58
+ browser.parent = null;
59
+ }
60
+ }
61
+ else if (!browser.parent) {
62
+ app.shell.add(browser, 'left', { rank: 2 });
63
+ if (settled) {
64
+ app.shell.activateById(browser.id);
65
+ }
66
+ }
67
+ });
68
+ browser.announceSections();
69
+ }
37
70
  if (settingRegistry) {
38
71
  settingRegistry.load(PLUGIN_ID).catch(reason => {
39
72
  console.error('Failed to load settings for xtralab.', reason);
@@ -1,13 +1,16 @@
1
+ import { IMovableSectionDestination, IMovableSectionSource, ISectionEntry } from '@jupyterlab/apputils';
1
2
  import { IDocumentManager } from '@jupyterlab/docmanager';
2
3
  import { Contents } from '@jupyterlab/services';
4
+ import { ITranslator } from '@jupyterlab/translation';
3
5
  import { Toolbar } from '@jupyterlab/ui-components';
4
6
  import { ISignal } from '@lumino/signaling';
5
- import { Widget } from '@lumino/widgets';
7
+ import { AccordionPanel, Widget } from '@lumino/widgets';
6
8
  export declare const FILE_BROWSER_ID = "xtralab:file-browser";
7
9
  interface IXtralabFileBrowserOptions {
8
10
  contentsManager: Contents.IManager;
9
11
  docManager: IDocumentManager;
10
12
  onOpenFile?: (serverPath: string) => void;
13
+ translator?: ITranslator;
11
14
  }
12
15
  /**
13
16
  * Public API of the xtralab file browser used by command handlers and other
@@ -119,13 +122,14 @@ export interface IXtralabFileBrowser {
119
122
  * Toggle the filter box above the tree.
120
123
  */
121
124
  toggleFileFilter(): void;
125
+ readonly sectionNode: HTMLElement;
122
126
  }
123
127
  /**
124
- * Lumino widget that hosts the React-based `@pierre/trees` file browser.
125
- * The widget is laid out with a JupyterLab `Toolbar` on top and the tree
126
- * filling the remaining space.
128
+ * Lumino widget hosting the React-based `@pierre/trees` file browser. The tree
129
+ * and its toolbar form one movable "Files" section, and sections from other
130
+ * panels can be moved in below it.
127
131
  */
128
- export declare class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
132
+ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBrowser, IMovableSectionSource, IMovableSectionDestination {
129
133
  constructor(options: IXtralabFileBrowserOptions);
130
134
  get contentsManager(): Contents.IManager;
131
135
  get toolbar(): Toolbar;
@@ -150,9 +154,39 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
150
154
  collapseAll(): void;
151
155
  setFileFilterVisible(visible: boolean): void;
152
156
  toggleFileFilter(): void;
157
+ get sectionNode(): HTMLElement;
158
+ get isEmpty(): boolean;
159
+ get contentChanged(): ISignal<this, void>;
160
+ get sectionAdded(): ISignal<this, ISectionEntry>;
161
+ get accordionPanel(): AccordionPanel | null;
162
+ get sections(): ReadonlyArray<Widget>;
163
+ /**
164
+ * The toolbar is the section's title node: it is the tree's header row at
165
+ * home, and an accordion host renders it inside the section title.
166
+ */
167
+ getSections(): ReadonlyArray<ISectionEntry>;
168
+ removeSectionById(sectionId: string): Widget | null;
169
+ reinsertSection(widget: Widget): void;
170
+ /**
171
+ * The move plugin reconciles persisted moves only against sections announced
172
+ * after `registerSource`, and this panel builds its section in the constructor.
173
+ */
174
+ announceSections(): void;
175
+ /**
176
+ * The first hosted section moves the tree into an accordion.
177
+ */
178
+ addSection(widget: Widget): void;
179
+ removeSectionWidget(widget: Widget): void;
180
+ private _isSectionHome;
181
+ /**
182
+ * An accordion host moves the toolbar node into the section title and leaves
183
+ * it orphaned on detach; re-parenting makes the layout attach it again.
184
+ */
185
+ private _restoreToolbar;
153
186
  private _contentsManager;
154
187
  private _docManager;
155
188
  private _onOpenFile;
189
+ private _translator;
156
190
  private _selectedPaths;
157
191
  private _selectionChanged;
158
192
  private _refreshRequested;
@@ -164,5 +198,10 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
164
198
  private _fileFilterVisibleChanged;
165
199
  private _toolbar;
166
200
  private _content;
201
+ private _section;
202
+ private _panelLayout;
203
+ private _accordion;
204
+ private _contentChanged;
205
+ private _sectionAdded;
167
206
  }
168
207
  export {};