@hienlh/ppm 0.11.2 → 0.11.3
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/CHANGELOG.md +6 -0
- package/dist/web/assets/{chat-tab-DYf6U6UF.js → chat-tab-1Khvyjvx.js} +3 -3
- package/dist/web/assets/{code-editor-BPxBeu0S.js → code-editor-BtOD0A1r.js} +2 -2
- package/dist/web/assets/{conflict-editor-BCkYHDUy.js → conflict-editor-DsLm2d9j.js} +1 -1
- package/dist/web/assets/{database-viewer-CCe8qa1Q.js → database-viewer-Bf-FjggD.js} +1 -1
- package/dist/web/assets/{diff-viewer-DIjzWvaG.js → diff-viewer-DZcLi76X.js} +1 -1
- package/dist/web/assets/{extension-webview-HY8XueLo.js → extension-webview-Dt50AKjl.js} +1 -1
- package/dist/web/assets/index-CwVBevJP.js +26 -0
- package/dist/web/assets/{markdown-renderer-BQV0AIm5.js → markdown-renderer-INc5L7kL.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-DPmTpfFX.js → port-forwarding-tab-Ds8rV7YG.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BUSNt_7x.js → postgres-viewer-Cvedgnv5.js} +1 -1
- package/dist/web/assets/{settings-tab-DHBG5O0C.js → settings-tab-D6jFbyjn.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-B7WnFN29.js → sqlite-viewer-CXbf9WUf.js} +1 -1
- package/dist/web/assets/{terminal-tab-1K4ijyNe.js → terminal-tab-CGu6B-nW.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/files.ts +39 -1
- package/src/web/components/explorer/file-tree.tsx +107 -4
- package/dist/web/assets/index-DpRxWGjM.js +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useCallback, useState, memo } from "react";
|
|
1
|
+
import { useEffect, useCallback, useState, useRef, memo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Folder,
|
|
4
4
|
FolderOpen,
|
|
@@ -30,6 +30,12 @@ import {
|
|
|
30
30
|
} from "@/components/ui/context-menu";
|
|
31
31
|
import { FileActions } from "./file-actions";
|
|
32
32
|
import { downloadFile, downloadFolder } from "@/lib/file-download";
|
|
33
|
+
import { getAuthToken, projectUrl } from "@/lib/api-client";
|
|
34
|
+
|
|
35
|
+
/** Check if drag event is from OS files (not internal PPM drag) */
|
|
36
|
+
function isExternalFileDrag(e: React.DragEvent): boolean {
|
|
37
|
+
return e.dataTransfer.types.includes("Files") && !e.dataTransfer.types.includes("application/x-ppm-path");
|
|
38
|
+
}
|
|
33
39
|
|
|
34
40
|
/** Synthetic root node for creating files/folders at project root */
|
|
35
41
|
const ROOT_NODE: FileNode = { name: "", path: "", type: "directory" };
|
|
@@ -62,16 +68,19 @@ interface TreeNodeProps {
|
|
|
62
68
|
depth: number;
|
|
63
69
|
projectName: string;
|
|
64
70
|
onAction: (action: string, node: FileNode) => void;
|
|
71
|
+
onFileDrop: (targetDir: string, files: FileList) => void;
|
|
65
72
|
onFileOpen?: () => void;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, onFileOpen }: TreeNodeProps) {
|
|
75
|
+
const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, onFileDrop, onFileOpen }: TreeNodeProps) {
|
|
69
76
|
const { expandedPaths, toggleExpand, selectedFiles, toggleFileSelect } = useFileStore(useShallow((s) => ({ expandedPaths: s.expandedPaths, toggleExpand: s.toggleExpand, selectedFiles: s.selectedFiles, toggleFileSelect: s.toggleFileSelect })));
|
|
70
77
|
const openTab = useTabStore((s) => s.openTab);
|
|
71
78
|
const isExpanded = expandedPaths.has(node.path);
|
|
72
79
|
const isDir = node.type === "directory";
|
|
73
80
|
const isSelected = selectedFiles.includes(node.path);
|
|
74
81
|
const isIgnored = node.ignored === true;
|
|
82
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
83
|
+
const dragCounter = useRef(0);
|
|
75
84
|
|
|
76
85
|
function handleClick(e: React.MouseEvent) {
|
|
77
86
|
if (isDir) {
|
|
@@ -102,6 +111,36 @@ const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, on
|
|
|
102
111
|
e.dataTransfer.effectAllowed = "copy";
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function handleNodeDragEnter(e: React.DragEvent) {
|
|
115
|
+
if (!isDir || !isExternalFileDrag(e)) return;
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
dragCounter.current++;
|
|
119
|
+
if (dragCounter.current === 1) setIsDragOver(true);
|
|
120
|
+
}
|
|
121
|
+
function handleNodeDragLeave(e: React.DragEvent) {
|
|
122
|
+
if (!isDir) return;
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
dragCounter.current--;
|
|
125
|
+
if (dragCounter.current === 0) setIsDragOver(false);
|
|
126
|
+
}
|
|
127
|
+
function handleNodeDragOver(e: React.DragEvent) {
|
|
128
|
+
if (!isDir || !isExternalFileDrag(e)) return;
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
e.dataTransfer.dropEffect = "copy";
|
|
132
|
+
}
|
|
133
|
+
function handleNodeDrop(e: React.DragEvent) {
|
|
134
|
+
if (!isDir || !isExternalFileDrag(e)) return;
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
dragCounter.current = 0;
|
|
138
|
+
setIsDragOver(false);
|
|
139
|
+
if (e.dataTransfer.files.length > 0) {
|
|
140
|
+
onFileDrop(node.path, e.dataTransfer.files);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
const Icon = isDir
|
|
106
145
|
? isExpanded
|
|
107
146
|
? FolderOpen
|
|
@@ -116,7 +155,12 @@ const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, on
|
|
|
116
155
|
: [];
|
|
117
156
|
|
|
118
157
|
return (
|
|
119
|
-
<div
|
|
158
|
+
<div
|
|
159
|
+
onDragEnter={isDir ? handleNodeDragEnter : undefined}
|
|
160
|
+
onDragLeave={isDir ? handleNodeDragLeave : undefined}
|
|
161
|
+
onDragOver={isDir ? handleNodeDragOver : undefined}
|
|
162
|
+
onDrop={isDir ? handleNodeDrop : undefined}
|
|
163
|
+
>
|
|
120
164
|
<ContextMenu>
|
|
121
165
|
<ContextMenuTrigger asChild>
|
|
122
166
|
<button
|
|
@@ -129,6 +173,7 @@ const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, on
|
|
|
129
173
|
"select-none",
|
|
130
174
|
isIgnored && "opacity-40",
|
|
131
175
|
isSelected && "bg-primary/15 ring-1 ring-primary/40",
|
|
176
|
+
isDragOver && "ring-1 ring-dashed ring-primary bg-primary/10",
|
|
132
177
|
)}
|
|
133
178
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
134
179
|
>
|
|
@@ -198,6 +243,7 @@ const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, on
|
|
|
198
243
|
depth={depth + 1}
|
|
199
244
|
projectName={projectName}
|
|
200
245
|
onAction={onAction}
|
|
246
|
+
onFileDrop={onFileDrop}
|
|
201
247
|
onFileOpen={onFileOpen}
|
|
202
248
|
/>
|
|
203
249
|
))}
|
|
@@ -239,6 +285,56 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
|
|
|
239
285
|
return () => window.removeEventListener("focus", handleFocus);
|
|
240
286
|
}, [activeProject, fetchTree]);
|
|
241
287
|
|
|
288
|
+
const uploadFiles = useCallback(async (targetDir: string, files: FileList) => {
|
|
289
|
+
if (!activeProject) return;
|
|
290
|
+
const form = new FormData();
|
|
291
|
+
form.append("targetDir", targetDir);
|
|
292
|
+
for (const file of files) form.append("files", file);
|
|
293
|
+
const headers: HeadersInit = {};
|
|
294
|
+
const token = getAuthToken();
|
|
295
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(`${projectUrl(activeProject.name)}/files/upload`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers,
|
|
300
|
+
body: form,
|
|
301
|
+
});
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const json = await res.json();
|
|
304
|
+
console.error("Upload failed:", json.error);
|
|
305
|
+
}
|
|
306
|
+
loadTree();
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.error("Upload error:", e);
|
|
309
|
+
}
|
|
310
|
+
}, [activeProject, loadTree]);
|
|
311
|
+
|
|
312
|
+
const [isRootDragOver, setIsRootDragOver] = useState(false);
|
|
313
|
+
const rootDragCounter = useRef(0);
|
|
314
|
+
|
|
315
|
+
function handleRootDragEnter(e: React.DragEvent) {
|
|
316
|
+
if (!isExternalFileDrag(e)) return;
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
rootDragCounter.current++;
|
|
319
|
+
if (rootDragCounter.current === 1) setIsRootDragOver(true);
|
|
320
|
+
}
|
|
321
|
+
function handleRootDragLeave() {
|
|
322
|
+
rootDragCounter.current--;
|
|
323
|
+
if (rootDragCounter.current === 0) setIsRootDragOver(false);
|
|
324
|
+
}
|
|
325
|
+
function handleRootDragOver(e: React.DragEvent) {
|
|
326
|
+
if (!isExternalFileDrag(e)) return;
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
e.dataTransfer.dropEffect = "copy";
|
|
329
|
+
}
|
|
330
|
+
function handleRootDrop(e: React.DragEvent) {
|
|
331
|
+
if (!isExternalFileDrag(e)) return;
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
rootDragCounter.current = 0;
|
|
334
|
+
setIsRootDragOver(false);
|
|
335
|
+
if (e.dataTransfer.files.length > 0) uploadFiles("", e.dataTransfer.files);
|
|
336
|
+
}
|
|
337
|
+
|
|
242
338
|
function handleAction(action: string, node: FileNode) {
|
|
243
339
|
if (action === "copy-path") {
|
|
244
340
|
navigator.clipboard.writeText(node.path).catch(() => {});
|
|
@@ -310,7 +406,13 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
|
|
|
310
406
|
const toolbarBtnClass = "p-1 rounded-sm text-text-secondary hover:text-foreground hover:bg-surface-elevated transition-colors";
|
|
311
407
|
|
|
312
408
|
return (
|
|
313
|
-
<div
|
|
409
|
+
<div
|
|
410
|
+
className={cn("flex flex-col h-full", isRootDragOver && "bg-primary/5")}
|
|
411
|
+
onDragEnter={handleRootDragEnter}
|
|
412
|
+
onDragLeave={handleRootDragLeave}
|
|
413
|
+
onDragOver={handleRootDragOver}
|
|
414
|
+
onDrop={handleRootDrop}
|
|
415
|
+
>
|
|
314
416
|
{/* Toolbar */}
|
|
315
417
|
<div className="flex items-center gap-0.5 px-2 h-8 border-b border-border shrink-0">
|
|
316
418
|
<button onClick={() => handleAction("new-file", ROOT_NODE)} title="New File" className={toolbarBtnClass}>
|
|
@@ -337,6 +439,7 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
|
|
|
337
439
|
depth={0}
|
|
338
440
|
projectName={activeProject.name}
|
|
339
441
|
onAction={handleAction}
|
|
442
|
+
onFileDrop={uploadFiles}
|
|
340
443
|
onFileOpen={onFileOpen}
|
|
341
444
|
/>
|
|
342
445
|
))}
|