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.
- package/lib/customPanel/index.d.ts +7 -0
- package/lib/customPanel/index.js +74 -0
- package/lib/fileBrowser/commands.js +13 -1
- package/lib/fileBrowser/contents.d.ts +11 -0
- package/lib/fileBrowser/contents.js +28 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
- package/lib/fileBrowser/dragAndDrop.js +115 -0
- package/lib/fileBrowser/fileBrowser.d.ts +2 -0
- package/lib/fileBrowser/fileBrowser.js +151 -197
- package/lib/fileBrowser/index.js +39 -6
- package/lib/fileBrowser/widget.d.ts +44 -5
- package/lib/fileBrowser/widget.js +153 -14
- package/lib/index.js +4 -0
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +15 -2
- package/lib/launcher/icons.js +15 -4
- package/lib/launcher/schemaDefaults.d.ts +2 -9
- package/lib/launcher/schemaDefaults.js +37 -13
- package/lib/menuBar/icons.d.ts +6 -0
- package/lib/menuBar/icons.js +13 -0
- package/lib/menuBar/index.d.ts +15 -0
- package/lib/menuBar/index.js +164 -0
- package/lib/sidebar/index.d.ts +7 -13
- package/lib/sidebar/index.js +80 -46
- package/lib/terminals/index.d.ts +5 -3
- package/lib/terminals/index.js +17 -6
- package/lib/terminals/widget.d.ts +29 -25
- package/lib/terminals/widget.js +150 -65
- package/package.json +1 -1
- package/schema/launcher.json +2 -2
- package/schema/menu-bar.json +37 -0
- package/schema/sidebar.json +47 -7
- package/src/customPanel/index.ts +104 -0
- package/src/fileBrowser/commands.ts +14 -1
- package/src/fileBrowser/contents.ts +30 -0
- package/src/fileBrowser/dragAndDrop.ts +178 -0
- package/src/fileBrowser/fileBrowser.tsx +183 -225
- package/src/fileBrowser/index.ts +41 -6
- package/src/fileBrowser/widget.tsx +195 -14
- package/src/index.ts +4 -0
- package/src/launcher/agents.ts +16 -3
- package/src/launcher/icons.ts +16 -4
- package/src/launcher/schemaDefaults.ts +43 -13
- package/src/menuBar/icons.ts +14 -0
- package/src/menuBar/index.ts +195 -0
- package/src/sidebar/index.ts +95 -52
- package/src/terminals/index.ts +24 -6
- package/src/terminals/widget.tsx +186 -87
- package/style/base.css +22 -34
- package/style/customPanel.css +9 -0
- package/style/index.js +1 -0
- package/style/terminals.css +19 -77
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
}
|
package/lib/fileBrowser/index.js
CHANGED
|
@@ -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: [
|
|
19
|
-
|
|
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
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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 {};
|