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,22 +1,36 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { showErrorMessage } from '@jupyterlab/apputils';
|
|
4
|
+
import { IDocumentManager, renameFile } from '@jupyterlab/docmanager';
|
|
4
5
|
import { Contents } from '@jupyterlab/services';
|
|
5
|
-
import {
|
|
6
|
-
import { MimeData, PromiseDelegate } from '@lumino/coreutils';
|
|
7
|
-
import { Drag } from '@lumino/dragdrop';
|
|
6
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
8
7
|
import { Poll } from '@lumino/polling';
|
|
9
8
|
|
|
10
9
|
import { FileTree, useFileTree } from '@pierre/trees/react';
|
|
11
10
|
import type {
|
|
12
11
|
FileTreeBatchOperation,
|
|
13
12
|
FileTreeDirectoryHandle,
|
|
13
|
+
FileTreeDropContext,
|
|
14
|
+
FileTreeDropResult,
|
|
14
15
|
GitStatusEntry
|
|
15
16
|
} from '@pierre/trees';
|
|
16
17
|
|
|
17
18
|
import type { Ignore } from 'ignore';
|
|
18
19
|
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
ROOT_LOAD_KEY,
|
|
22
|
+
listDirectory,
|
|
23
|
+
parentOf,
|
|
24
|
+
toServerPath
|
|
25
|
+
} from './contents';
|
|
26
|
+
import {
|
|
27
|
+
IDropMove,
|
|
28
|
+
ITreeDropHandler,
|
|
29
|
+
computeDropMoves,
|
|
30
|
+
computeRootDropMoves,
|
|
31
|
+
createTreeDragAndDropConfig,
|
|
32
|
+
useRootDropZone
|
|
33
|
+
} from './dragAndDrop';
|
|
20
34
|
import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
|
|
21
35
|
import { loadGitStatusEntries } from './gitStatus';
|
|
22
36
|
import { FILE_BROWSER_ICONS } from './icons';
|
|
@@ -29,23 +43,6 @@ import type { XtralabFileBrowser } from './widget';
|
|
|
29
43
|
*/
|
|
30
44
|
type LoadState = 'unloaded' | 'loading' | 'loaded';
|
|
31
45
|
|
|
32
|
-
/**
|
|
33
|
-
* MIME types used by the dock panel and other JupyterLab drop targets.
|
|
34
|
-
* `FACTORY_MIME` is the lumino dock-panel contract: the value must be a
|
|
35
|
-
* synchronous function that returns a Widget. The contents MIME types
|
|
36
|
-
* mirror what the default file browser sends so other drop targets that
|
|
37
|
-
* understand them (the file browser itself, custom drop zones) keep
|
|
38
|
-
* working when the user drags out of xtralab.
|
|
39
|
-
*/
|
|
40
|
-
const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
|
|
41
|
-
const CONTENTS_MIME = 'application/x-jupyter-icontents';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Threshold in pixels before a press-and-drag is treated as a drag rather
|
|
45
|
-
* than a click. Matches the value used by the default JupyterLab listing.
|
|
46
|
-
*/
|
|
47
|
-
const DRAG_THRESHOLD = 5;
|
|
48
|
-
|
|
49
46
|
/**
|
|
50
47
|
* Polling cadence for the git status decoration. Out-of-band changes (a
|
|
51
48
|
* terminal `git add`, a `git pull`, a file edited outside the JupyterLab
|
|
@@ -98,7 +95,10 @@ const FILE_TREE_TAG = 'file-tree-container';
|
|
|
98
95
|
* box unless the host carries the visibility marker set by the filter
|
|
99
96
|
* bridge effect below — `@pierre/trees` always renders the box when
|
|
100
97
|
* `search` is enabled, and its stylesheet lives in the shadow root where
|
|
101
|
-
* outside CSS cannot reach.
|
|
98
|
+
* outside CSS cannot reach. The third rule restyles the drag-hover row:
|
|
99
|
+
* the library paints it with the selection background, which xtralab
|
|
100
|
+
* maps to a full-strength brand color that is illegible without the
|
|
101
|
+
* inverted selection foreground — hence the quieter ring.
|
|
102
102
|
*/
|
|
103
103
|
const FILE_TREE_UNSAFE_CSS =
|
|
104
104
|
'[data-type="item"][data-item-selected="true"] ' +
|
|
@@ -108,12 +108,17 @@ const FILE_TREE_UNSAFE_CSS =
|
|
|
108
108
|
':host(:not([data-xtralab-filter-visible])) ' +
|
|
109
109
|
'[data-file-tree-search-container] {' +
|
|
110
110
|
'display: none;' +
|
|
111
|
+
'}' +
|
|
112
|
+
'[data-type="item"][data-item-drag-target="true"] {' +
|
|
113
|
+
'background-color: var(--trees-bg-muted);' +
|
|
114
|
+
'box-shadow: inset 0 0 0 2px var(--trees-accent);' +
|
|
111
115
|
'}';
|
|
112
116
|
|
|
113
117
|
interface IFileBrowserProps {
|
|
114
118
|
contentsManager: Contents.IManager;
|
|
115
119
|
docManager: IDocumentManager;
|
|
116
120
|
onOpenFile?: (serverPath: string) => void;
|
|
121
|
+
translator?: ITranslator;
|
|
117
122
|
/**
|
|
118
123
|
* The host widget. Selection-change events are pushed up so context-menu
|
|
119
124
|
* commands can react to what the user has selected.
|
|
@@ -133,7 +138,14 @@ interface IFileBrowserProps {
|
|
|
133
138
|
export function FileBrowserComponent(
|
|
134
139
|
props: IFileBrowserProps
|
|
135
140
|
): React.ReactElement {
|
|
136
|
-
const { contentsManager, docManager, onOpenFile, widget } = props;
|
|
141
|
+
const { contentsManager, docManager, onOpenFile, translator, widget } = props;
|
|
142
|
+
|
|
143
|
+
const trans = React.useMemo(
|
|
144
|
+
() => (translator ?? nullTranslator).load('jupyterlab'),
|
|
145
|
+
[translator]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const dropHandlerRef = React.useRef<ITreeDropHandler | null>(null);
|
|
137
149
|
|
|
138
150
|
const { model } = useFileTree({
|
|
139
151
|
paths: [],
|
|
@@ -141,7 +153,8 @@ export function FileBrowserComponent(
|
|
|
141
153
|
search: true,
|
|
142
154
|
icons: FILE_BROWSER_ICONS,
|
|
143
155
|
itemHeight: 24,
|
|
144
|
-
unsafeCSS: FILE_TREE_UNSAFE_CSS
|
|
156
|
+
unsafeCSS: FILE_TREE_UNSAFE_CSS,
|
|
157
|
+
dragAndDrop: createTreeDragAndDropConfig(dropHandlerRef)
|
|
145
158
|
});
|
|
146
159
|
|
|
147
160
|
React.useEffect(() => {
|
|
@@ -275,21 +288,6 @@ export function FileBrowserComponent(
|
|
|
275
288
|
}
|
|
276
289
|
};
|
|
277
290
|
|
|
278
|
-
/**
|
|
279
|
-
* Compute the canonical parent path for a given canonical path.
|
|
280
|
-
* Returns {@link ROOT_LOAD_KEY} for top-level entries.
|
|
281
|
-
*/
|
|
282
|
-
const parentOf = (canonicalPath: string): string => {
|
|
283
|
-
const trimmed = canonicalPath.endsWith('/')
|
|
284
|
-
? canonicalPath.slice(0, -1)
|
|
285
|
-
: canonicalPath;
|
|
286
|
-
const idx = trimmed.lastIndexOf('/');
|
|
287
|
-
if (idx < 0) {
|
|
288
|
-
return ROOT_LOAD_KEY;
|
|
289
|
-
}
|
|
290
|
-
return `${trimmed.slice(0, idx)}/`;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
291
|
/**
|
|
294
292
|
* Refresh every directory currently loaded into the tree. Walks down
|
|
295
293
|
* from the root through the previously-expanded subdirectories so the
|
|
@@ -712,6 +710,148 @@ export function FileBrowserComponent(
|
|
|
712
710
|
}
|
|
713
711
|
};
|
|
714
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Re-key the load-state mirrors after the tree model moved
|
|
715
|
+
* `fromPath` to `toPath`; a moved directory rewrites every entry
|
|
716
|
+
* under its old prefix.
|
|
717
|
+
*/
|
|
718
|
+
const rekeyMirrors = (fromPath: string, toPath: string): void => {
|
|
719
|
+
if (fromPath.endsWith('/')) {
|
|
720
|
+
for (const path of [...loadedPaths]) {
|
|
721
|
+
if (path.startsWith(fromPath)) {
|
|
722
|
+
loadedPaths.delete(path);
|
|
723
|
+
loadedPaths.add(`${toPath}${path.slice(fromPath.length)}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
for (const [dir, state] of [...knownDirs.entries()]) {
|
|
727
|
+
if (dir !== ROOT_LOAD_KEY && dir.startsWith(fromPath)) {
|
|
728
|
+
knownDirs.delete(dir);
|
|
729
|
+
knownDirs.set(`${toPath}${dir.slice(fromPath.length)}`, state);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else if (loadedPaths.delete(fromPath)) {
|
|
733
|
+
loadedPaths.add(toPath);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Rename through the document manager, so collisions surface the
|
|
739
|
+
* standard overwrite dialog and open widgets follow their file.
|
|
740
|
+
* Resolves false when the move did not happen — a declined
|
|
741
|
+
* overwrite (upstream's 'File not renamed' sentinel) or a failure.
|
|
742
|
+
*/
|
|
743
|
+
const renameOnServer = async (
|
|
744
|
+
fromPath: string,
|
|
745
|
+
toPath: string
|
|
746
|
+
): Promise<boolean> => {
|
|
747
|
+
try {
|
|
748
|
+
await renameFile(
|
|
749
|
+
docManager,
|
|
750
|
+
toServerPath(fromPath),
|
|
751
|
+
toServerPath(toPath)
|
|
752
|
+
);
|
|
753
|
+
return true;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
if (err !== 'File not renamed') {
|
|
756
|
+
await showErrorMessage(trans.__('Move failed'), err as Error);
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Persist moves the tree model already applied. A move that did
|
|
764
|
+
* not reach the disk leaves the tree out of step, so a full
|
|
765
|
+
* refresh then restores server truth.
|
|
766
|
+
*/
|
|
767
|
+
const persistDropMoves = async (moves: IDropMove[]): Promise<void> => {
|
|
768
|
+
let failed = false;
|
|
769
|
+
for (const move of moves) {
|
|
770
|
+
if (!(await renameOnServer(move.from, move.to))) {
|
|
771
|
+
failed = true;
|
|
772
|
+
}
|
|
773
|
+
if (cancelled) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (failed) {
|
|
778
|
+
void refreshAll();
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const completeDrop = (result: FileTreeDropResult): void => {
|
|
783
|
+
if (cancelled) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const moves = computeDropMoves(result);
|
|
787
|
+
for (const move of moves) {
|
|
788
|
+
rekeyMirrors(move.from, move.to);
|
|
789
|
+
}
|
|
790
|
+
syncGitStatus();
|
|
791
|
+
void persistDropMoves(moves);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Server-side renames for moves the tree model did not apply; the
|
|
796
|
+
* `fileChanged` signals nudge the listing poll to reconcile.
|
|
797
|
+
*/
|
|
798
|
+
const persistFallbackMoves = async (moves: IDropMove[]): Promise<void> => {
|
|
799
|
+
for (const move of moves) {
|
|
800
|
+
await renameOnServer(move.from, move.to);
|
|
801
|
+
if (cancelled) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* The tree refused the drop and left its model untouched — almost
|
|
809
|
+
* always a name collision at the destination.
|
|
810
|
+
*/
|
|
811
|
+
const failedDrop = (error: string, context: FileTreeDropContext): void => {
|
|
812
|
+
if (cancelled) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const moves = computeDropMoves(context);
|
|
816
|
+
if (moves.length === 0) {
|
|
817
|
+
console.error('xtralab: drop failed', error);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
void persistFallbackMoves(moves);
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Move the dragged paths to the workspace root: apply to the model
|
|
825
|
+
* first so the rows relocate instantly, then persist. A path the
|
|
826
|
+
* model cannot move (name collision) falls back to the server-side
|
|
827
|
+
* rename and its dialog.
|
|
828
|
+
*/
|
|
829
|
+
const dropOnRoot = (draggedPaths: readonly string[]): void => {
|
|
830
|
+
if (cancelled) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const applied: IDropMove[] = [];
|
|
834
|
+
const fallback: IDropMove[] = [];
|
|
835
|
+
for (const move of computeRootDropMoves(draggedPaths)) {
|
|
836
|
+
try {
|
|
837
|
+
model.move(move.from, move.to);
|
|
838
|
+
rekeyMirrors(move.from, move.to);
|
|
839
|
+
applied.push(move);
|
|
840
|
+
} catch {
|
|
841
|
+
fallback.push(move);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (applied.length > 0) {
|
|
845
|
+
syncGitStatus();
|
|
846
|
+
void persistDropMoves(applied);
|
|
847
|
+
}
|
|
848
|
+
if (fallback.length > 0) {
|
|
849
|
+
void persistFallbackMoves(fallback);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
dropHandlerRef.current = { completeDrop, failedDrop, dropOnRoot };
|
|
854
|
+
|
|
715
855
|
knownDirs.set(ROOT_LOAD_KEY, 'unloaded');
|
|
716
856
|
void fetchDirectory(ROOT_LOAD_KEY);
|
|
717
857
|
void refreshGitignoreMatcher();
|
|
@@ -818,6 +958,7 @@ export function FileBrowserComponent(
|
|
|
818
958
|
|
|
819
959
|
return () => {
|
|
820
960
|
cancelled = true;
|
|
961
|
+
dropHandlerRef.current = null;
|
|
821
962
|
gitStatusPoll.dispose();
|
|
822
963
|
contentsManager.fileChanged.disconnect(onContentsFileChanged);
|
|
823
964
|
listingPoll.dispose();
|
|
@@ -840,7 +981,7 @@ export function FileBrowserComponent(
|
|
|
840
981
|
}
|
|
841
982
|
}
|
|
842
983
|
};
|
|
843
|
-
}, [model, contentsManager, widget]);
|
|
984
|
+
}, [model, contentsManager, widget, docManager, trans]);
|
|
844
985
|
|
|
845
986
|
// Bridge the tree's selection state up to the widget so command handlers
|
|
846
987
|
// can read it without depending on React internals. The tree exposes its
|
|
@@ -1000,168 +1141,7 @@ export function FileBrowserComponent(
|
|
|
1000
1141
|
};
|
|
1001
1142
|
}, []);
|
|
1002
1143
|
|
|
1003
|
-
|
|
1004
|
-
// lives in the `@pierre/trees` shadow DOM, so we listen to mousedown on
|
|
1005
|
-
// the wrapper and walk `composedPath()` to recover the row. The drag
|
|
1006
|
-
// payload uses Lumino's `FACTORY_MIME` contract: the dock panel calls the
|
|
1007
|
-
// factory function on drop and adds the returned widget to its layout.
|
|
1008
|
-
React.useEffect(() => {
|
|
1009
|
-
const wrapper = wrapperRef.current;
|
|
1010
|
-
if (wrapper === null) {
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
let press: { x: number; y: number; path: string } | null = null;
|
|
1015
|
-
let activeDrag: Drag | null = null;
|
|
1016
|
-
|
|
1017
|
-
const findRow = (event: MouseEvent): HTMLElement | null => {
|
|
1018
|
-
for (const target of event.composedPath()) {
|
|
1019
|
-
if (!(target instanceof HTMLElement)) {
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
if (target.dataset.type === 'item') {
|
|
1023
|
-
return target;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
return null;
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
|
-
const cleanup = (): void => {
|
|
1030
|
-
press = null;
|
|
1031
|
-
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
1032
|
-
document.removeEventListener('mouseup', handleMouseUp, true);
|
|
1033
|
-
};
|
|
1034
|
-
|
|
1035
|
-
const handleMouseDown = (event: MouseEvent): void => {
|
|
1036
|
-
// Only react to the primary button. Do not interfere with right-click
|
|
1037
|
-
// (handled by the contextmenu bridge) or middle-click (browser default).
|
|
1038
|
-
if (event.button !== 0) {
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
if (activeDrag !== null) {
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
const row = findRow(event);
|
|
1045
|
-
if (row === null || row.dataset.itemType !== 'file') {
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
const itemPath = row.dataset.itemPath;
|
|
1049
|
-
if (itemPath === undefined || itemPath.length === 0) {
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
press = { x: event.clientX, y: event.clientY, path: itemPath };
|
|
1053
|
-
document.addEventListener('mousemove', handleMouseMove, true);
|
|
1054
|
-
document.addEventListener('mouseup', handleMouseUp, true);
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
const handleMouseMove = (event: MouseEvent): void => {
|
|
1058
|
-
if (press === null) {
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const dx = Math.abs(event.clientX - press.x);
|
|
1062
|
-
const dy = Math.abs(event.clientY - press.y);
|
|
1063
|
-
if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
const startedFrom = press;
|
|
1067
|
-
cleanup();
|
|
1068
|
-
startDrag(startedFrom.path, event.clientX, event.clientY);
|
|
1069
|
-
};
|
|
1070
|
-
|
|
1071
|
-
const handleMouseUp = (): void => {
|
|
1072
|
-
cleanup();
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Build the list of file paths to drag. If the user pressed on a row
|
|
1077
|
-
* that is part of the current selection, drag every selected file.
|
|
1078
|
-
* Otherwise, drag just the row that was pressed. Folders never join a
|
|
1079
|
-
* drag-out — they have no docmanager widget to open.
|
|
1080
|
-
*/
|
|
1081
|
-
const collectDragPaths = (sourcePath: string): string[] => {
|
|
1082
|
-
const selection = model.getSelectedPaths();
|
|
1083
|
-
const sourceInSelection = selection.includes(sourcePath);
|
|
1084
|
-
const candidates = sourceInSelection ? selection : [sourcePath];
|
|
1085
|
-
const fileServerPaths: string[] = [];
|
|
1086
|
-
for (const candidate of candidates) {
|
|
1087
|
-
if (candidate.endsWith('/')) {
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
fileServerPaths.push(toServerPath(candidate));
|
|
1091
|
-
}
|
|
1092
|
-
// The source row is always a file, so the list cannot be empty.
|
|
1093
|
-
if (fileServerPaths.length === 0) {
|
|
1094
|
-
fileServerPaths.push(toServerPath(sourcePath));
|
|
1095
|
-
}
|
|
1096
|
-
return fileServerPaths;
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
const startDrag = (
|
|
1100
|
-
sourcePath: string,
|
|
1101
|
-
clientX: number,
|
|
1102
|
-
clientY: number
|
|
1103
|
-
): void => {
|
|
1104
|
-
const paths = collectDragPaths(sourcePath);
|
|
1105
|
-
const sourceServerPath = toServerPath(sourcePath);
|
|
1106
|
-
|
|
1107
|
-
const dragImage = createDragImage(paths.length);
|
|
1108
|
-
|
|
1109
|
-
const drag = new Drag({
|
|
1110
|
-
dragImage,
|
|
1111
|
-
mimeData: new MimeData(),
|
|
1112
|
-
supportedActions: 'copy-move',
|
|
1113
|
-
proposedAction: 'move'
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
drag.mimeData.setData(CONTENTS_MIME, paths);
|
|
1117
|
-
|
|
1118
|
-
// The factory is called by the lumino dock panel on drop. It must
|
|
1119
|
-
// return a Widget synchronously. For multi-file drags we open the
|
|
1120
|
-
// remaining files asynchronously after the first one is placed,
|
|
1121
|
-
// mirroring the default file browser's behavior.
|
|
1122
|
-
const otherPaths = paths.filter(p => p !== sourceServerPath);
|
|
1123
|
-
drag.mimeData.setData(FACTORY_MIME, () => {
|
|
1124
|
-
let widget = docManager.findWidget(sourceServerPath);
|
|
1125
|
-
if (widget === undefined) {
|
|
1126
|
-
widget = docManager.open(sourceServerPath);
|
|
1127
|
-
}
|
|
1128
|
-
if (otherPaths.length > 0) {
|
|
1129
|
-
const firstPlaced = new PromiseDelegate<void>();
|
|
1130
|
-
void firstPlaced.promise.then(() => {
|
|
1131
|
-
let prev = widget;
|
|
1132
|
-
for (const otherPath of otherPaths) {
|
|
1133
|
-
const opened = docManager.openOrReveal(
|
|
1134
|
-
otherPath,
|
|
1135
|
-
undefined,
|
|
1136
|
-
undefined,
|
|
1137
|
-
prev !== undefined
|
|
1138
|
-
? { ref: prev.id, mode: 'tab-after' }
|
|
1139
|
-
: undefined
|
|
1140
|
-
);
|
|
1141
|
-
if (opened !== undefined) {
|
|
1142
|
-
prev = opened;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
});
|
|
1146
|
-
firstPlaced.resolve();
|
|
1147
|
-
}
|
|
1148
|
-
return widget;
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
activeDrag = drag;
|
|
1152
|
-
void drag.start(clientX, clientY).then(() => {
|
|
1153
|
-
activeDrag = null;
|
|
1154
|
-
});
|
|
1155
|
-
};
|
|
1156
|
-
|
|
1157
|
-
wrapper.addEventListener('mousedown', handleMouseDown);
|
|
1158
|
-
return () => {
|
|
1159
|
-
wrapper.removeEventListener('mousedown', handleMouseDown);
|
|
1160
|
-
cleanup();
|
|
1161
|
-
activeDrag?.dispose();
|
|
1162
|
-
activeDrag = null;
|
|
1163
|
-
};
|
|
1164
|
-
}, [model, docManager]);
|
|
1144
|
+
useRootDropZone({ model, handlerRef: dropHandlerRef, wrapperRef });
|
|
1165
1145
|
|
|
1166
1146
|
return (
|
|
1167
1147
|
<div
|
|
@@ -1172,25 +1152,3 @@ export function FileBrowserComponent(
|
|
|
1172
1152
|
</div>
|
|
1173
1153
|
);
|
|
1174
1154
|
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* Build the small badge shown next to the cursor while dragging. The
|
|
1178
|
-
* default file browser renders a richer image with the file's icon, but
|
|
1179
|
-
* we don't have a per-file icon at this layer — just the count of files
|
|
1180
|
-
* being dragged is enough to give the user feedback.
|
|
1181
|
-
*/
|
|
1182
|
-
function createDragImage(count: number): HTMLElement {
|
|
1183
|
-
const node = document.createElement('div');
|
|
1184
|
-
node.className = 'jp-xtralab-DragImage';
|
|
1185
|
-
const iconWrapper = document.createElement('span');
|
|
1186
|
-
iconWrapper.className = 'jp-xtralab-DragImage-icon';
|
|
1187
|
-
fileIcon.element({ container: iconWrapper, stylesheet: 'menuItem' });
|
|
1188
|
-
node.appendChild(iconWrapper);
|
|
1189
|
-
if (count > 1) {
|
|
1190
|
-
const badge = document.createElement('span');
|
|
1191
|
-
badge.className = 'jp-xtralab-DragImage-count';
|
|
1192
|
-
badge.textContent = String(count);
|
|
1193
|
-
node.appendChild(badge);
|
|
1194
|
-
}
|
|
1195
|
-
return node;
|
|
1196
|
-
}
|
package/src/fileBrowser/index.ts
CHANGED
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
JupyterFrontEndPlugin
|
|
5
5
|
} from '@jupyterlab/application';
|
|
6
6
|
|
|
7
|
-
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
7
|
+
import { ICommandPalette, IMovableSectionRegistry } from '@jupyterlab/apputils';
|
|
8
8
|
|
|
9
9
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
10
10
|
|
|
11
11
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
12
12
|
|
|
13
|
-
import { ITranslator } from '@jupyterlab/translation';
|
|
13
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
14
14
|
|
|
15
15
|
import { populateToolbar, registerCommands } from './commands';
|
|
16
|
-
import { XtralabFileBrowser } from './widget';
|
|
16
|
+
import { FILE_BROWSER_ID, XtralabFileBrowser } from './widget';
|
|
17
17
|
|
|
18
18
|
const PLUGIN_ID = 'xtralab:plugin';
|
|
19
19
|
|
|
@@ -27,21 +27,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
27
27
|
'A path-first file browser for JupyterLab built on @pierre/trees.',
|
|
28
28
|
autoStart: true,
|
|
29
29
|
requires: [IDocumentManager],
|
|
30
|
-
optional: [
|
|
30
|
+
optional: [
|
|
31
|
+
ILayoutRestorer,
|
|
32
|
+
ISettingRegistry,
|
|
33
|
+
ICommandPalette,
|
|
34
|
+
ITranslator,
|
|
35
|
+
IMovableSectionRegistry
|
|
36
|
+
],
|
|
31
37
|
activate: (
|
|
32
38
|
app: JupyterFrontEnd,
|
|
33
39
|
docManager: IDocumentManager,
|
|
34
40
|
restorer: ILayoutRestorer | null,
|
|
35
41
|
settingRegistry: ISettingRegistry | null,
|
|
36
42
|
palette: ICommandPalette | null,
|
|
37
|
-
translator: ITranslator | null
|
|
43
|
+
translator: ITranslator | null,
|
|
44
|
+
movableSections: IMovableSectionRegistry | null
|
|
38
45
|
): void => {
|
|
39
46
|
const browser = new XtralabFileBrowser({
|
|
40
47
|
contentsManager: app.serviceManager.contents,
|
|
41
48
|
docManager,
|
|
42
49
|
onOpenFile: (serverPath: string) => {
|
|
43
50
|
void app.commands.execute('docmanager:open', { path: serverPath });
|
|
44
|
-
}
|
|
51
|
+
},
|
|
52
|
+
translator: translator ?? undefined
|
|
45
53
|
});
|
|
46
54
|
// Ranks just under the Terminals panel in the left sidebar. The shipped
|
|
47
55
|
// `layout` setting assigns the default ranks (rank only, no area pin),
|
|
@@ -55,6 +63,33 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
55
63
|
registerCommands({ app, browser, docManager, palette, translator });
|
|
56
64
|
populateToolbar({ app, browser });
|
|
57
65
|
|
|
66
|
+
if (movableSections) {
|
|
67
|
+
const label = (translator ?? nullTranslator)
|
|
68
|
+
.load('jupyterlab')
|
|
69
|
+
.__('Files');
|
|
70
|
+
movableSections.registerSource(FILE_BROWSER_ID, label, browser);
|
|
71
|
+
movableSections.registerTarget(FILE_BROWSER_ID, label, browser);
|
|
72
|
+
|
|
73
|
+
// Only a user move should switch the sidebar, not the startup restoration.
|
|
74
|
+
let settled = false;
|
|
75
|
+
void app.restored.then(() => {
|
|
76
|
+
settled = true;
|
|
77
|
+
});
|
|
78
|
+
browser.contentChanged.connect(() => {
|
|
79
|
+
if (browser.isEmpty) {
|
|
80
|
+
if (browser.parent) {
|
|
81
|
+
browser.parent = null;
|
|
82
|
+
}
|
|
83
|
+
} else if (!browser.parent) {
|
|
84
|
+
app.shell.add(browser, 'left', { rank: 2 });
|
|
85
|
+
if (settled) {
|
|
86
|
+
app.shell.activateById(browser.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
browser.announceSections();
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
if (settingRegistry) {
|
|
59
94
|
settingRegistry.load(PLUGIN_ID).catch(reason => {
|
|
60
95
|
console.error('Failed to load settings for xtralab.', reason);
|