@shipstatic/drop 0.1.4 → 0.1.6
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/README.md +116 -161
- package/dist/index.cjs +108 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -22
- package/dist/index.d.ts +37 -22
- package/dist/index.js +110 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,27 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
A focused React hook for preparing files for deployment with [@shipstatic/ship](https://github.com/shipstatic/ship). Handles ZIP extraction, path normalization, and validation - everything needed before calling `ship.deploy()`.
|
|
6
6
|
|
|
7
|
+
**v0.1.6 Update:** Now includes built-in drag & drop support with prop getters! No need to manually implement drag & drop handlers - just spread `{...drop.getDropzoneProps()}` on your container and `{...drop.getInputProps()}` on the input element. Folder structure preservation is handled automatically.
|
|
8
|
+
|
|
7
9
|
**Note:** MD5 calculation is handled by Ship SDK during deployment. Drop focuses on file processing and UI state management.
|
|
8
10
|
|
|
9
11
|
## Why Headless?
|
|
10
12
|
|
|
11
|
-
This package provides **zero UI components
|
|
13
|
+
This package provides **zero UI components** - just a React hook with built-in drag & drop functionality. You bring your own styling.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
**What you get:**
|
|
16
|
+
1. **Built-in drag & drop** - Proper folder support with `webkitGetAsEntry` API, all handled internally
|
|
17
|
+
2. **Prop getters API** - Similar to `react-dropzone`, just spread props on your elements
|
|
18
|
+
3. **Full styling control** - No imposed CSS, design system, or theming
|
|
19
|
+
4. **Smaller bundle** - No UI components means less bloat
|
|
20
|
+
5. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
**What's different from other libraries:**
|
|
23
|
+
- Generic dropzone libraries don't preserve folder structure properly
|
|
24
|
+
- We handle the complex parts (ZIP extraction, folder traversal, path normalization)
|
|
25
|
+
- You handle the simple parts (styling, layout, animations)
|
|
19
26
|
|
|
20
27
|
## Features
|
|
21
28
|
|
|
22
|
-
- 🎯 **
|
|
29
|
+
- 🎯 **Prop Getters API** - Just spread props on your elements (like `react-dropzone`)
|
|
30
|
+
- 🖱️ **Built-in Drag & Drop** - Automatic folder support with `webkitGetAsEntry` API
|
|
23
31
|
- 📦 **ZIP Support** - Automatic ZIP file extraction and processing
|
|
24
32
|
- ✅ **Validation** - Client-side file size, count, and total size validation (powered by Ship SDK)
|
|
25
33
|
- 🗑️ **Junk Filtering** - Automatically filters `.DS_Store`, `Thumbs.db`, etc. (powered by Ship SDK)
|
|
26
34
|
- 🔒 **Path Sanitization** - Defense-in-depth protection against directory traversal attacks
|
|
27
|
-
- 📁 **Folder Structure Preservation** -
|
|
35
|
+
- 📁 **Folder Structure Preservation** - Proper folder paths via `webkitRelativePath`
|
|
36
|
+
- 🎨 **Headless UI** - No visual components, just logic and state management
|
|
28
37
|
- 🚀 **Focused Scope** - File processing and UI state only. MD5 calculation and deployment handled by Ship SDK
|
|
29
38
|
|
|
30
39
|
## Installation
|
|
@@ -50,30 +59,38 @@ function MyUploader() {
|
|
|
50
59
|
|
|
51
60
|
const handleUpload = async () => {
|
|
52
61
|
const validFiles = drop.getValidFiles();
|
|
53
|
-
|
|
54
62
|
// ProcessedFile extends StaticFile - no conversion needed!
|
|
55
63
|
await ship.deployments.create(validFiles.map(f => f.file));
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
return (
|
|
59
67
|
<div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
drop.
|
|
68
|
+
{/* Drag & drop zone with built-in folder support */}
|
|
69
|
+
<div
|
|
70
|
+
{...drop.getDropzoneProps()}
|
|
71
|
+
style={{
|
|
72
|
+
border: '2px dashed',
|
|
73
|
+
borderColor: drop.isDragging ? 'blue' : 'gray',
|
|
74
|
+
padding: '40px',
|
|
75
|
+
textAlign: 'center',
|
|
76
|
+
cursor: 'pointer',
|
|
66
77
|
}}
|
|
67
|
-
|
|
78
|
+
>
|
|
79
|
+
<input {...drop.getInputProps()} />
|
|
80
|
+
{drop.isDragging ? '📂 Drop here' : '📁 Click or drag files/folders'}
|
|
81
|
+
</div>
|
|
68
82
|
|
|
83
|
+
{/* Status */}
|
|
69
84
|
<p>{drop.statusText}</p>
|
|
70
85
|
|
|
86
|
+
{/* File list */}
|
|
71
87
|
{drop.files.map(file => (
|
|
72
88
|
<div key={file.id}>
|
|
73
89
|
{file.name} - {file.status}
|
|
74
90
|
</div>
|
|
75
91
|
))}
|
|
76
92
|
|
|
93
|
+
{/* Upload button */}
|
|
77
94
|
<button
|
|
78
95
|
onClick={handleUpload}
|
|
79
96
|
disabled={drop.getValidFiles().length === 0}
|
|
@@ -110,151 +127,56 @@ const drop = useDrop({ ship });
|
|
|
110
127
|
- Ship SDK is the single source of truth for validation
|
|
111
128
|
- Drop only provides what Ship doesn't have (ZIP, React state, folder structure)
|
|
112
129
|
|
|
113
|
-
##
|
|
130
|
+
## Advanced: Programmatic File Picker
|
|
114
131
|
|
|
115
|
-
|
|
132
|
+
You can programmatically trigger the file picker using the `open()` method:
|
|
116
133
|
|
|
117
134
|
```tsx
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
function MyDeployUI() {
|
|
122
|
-
const drop = useDrop();
|
|
123
|
-
const [isDragActive, setIsDragActive] = useState(false);
|
|
124
|
-
|
|
125
|
-
const handleDrop = async (e: React.DragEvent) => {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
setIsDragActive(false);
|
|
128
|
-
|
|
129
|
-
// Extract files with folder structure preserved
|
|
130
|
-
const files = await extractFilesWithStructure(e.dataTransfer);
|
|
131
|
-
drop.processFiles(files);
|
|
132
|
-
};
|
|
135
|
+
function MyUploader() {
|
|
136
|
+
const drop = useDrop({ ship });
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
{/* Custom trigger button */}
|
|
141
|
+
<button onClick={drop.open}>
|
|
142
|
+
Select Files
|
|
143
|
+
</button>
|
|
139
144
|
|
|
140
|
-
|
|
145
|
+
{/* Hidden input managed by the hook */}
|
|
146
|
+
<input {...drop.getInputProps()} />
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
{/* Or use the dropzone */}
|
|
149
|
+
<div {...drop.getDropzoneProps()}>
|
|
150
|
+
Drop files here
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
};
|
|
157
|
+
## Advanced: Manual File Processing
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
item: DataTransferItem,
|
|
154
|
-
files: File[]
|
|
155
|
-
): Promise<void> => {
|
|
156
|
-
// Try modern File System Access API first (Chrome 86+)
|
|
157
|
-
if (
|
|
158
|
-
globalThis.isSecureContext &&
|
|
159
|
-
typeof (item as any).getAsFileSystemHandle === 'function'
|
|
160
|
-
) {
|
|
161
|
-
try {
|
|
162
|
-
const handle = await (item as any).getAsFileSystemHandle();
|
|
163
|
-
if (handle) {
|
|
164
|
-
await processFileSystemHandle(handle, files, '');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
// Fall through to webkit API
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Fallback to webkitGetAsEntry (broader browser support)
|
|
173
|
-
const entry = (item as any).webkitGetAsEntry?.();
|
|
174
|
-
if (entry) {
|
|
175
|
-
await processEntry(entry, files, '');
|
|
176
|
-
}
|
|
177
|
-
};
|
|
159
|
+
For advanced use cases, you can manually process files instead of using prop getters:
|
|
178
160
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
basePath: string
|
|
183
|
-
): Promise<void> => {
|
|
184
|
-
if (handle.kind === 'file') {
|
|
185
|
-
const file = await handle.getFile();
|
|
186
|
-
// Set webkitRelativePath for Ship SDK compatibility
|
|
187
|
-
Object.defineProperty(file, 'webkitRelativePath', {
|
|
188
|
-
value: basePath + file.name,
|
|
189
|
-
writable: false,
|
|
190
|
-
enumerable: true,
|
|
191
|
-
configurable: true,
|
|
192
|
-
});
|
|
193
|
-
files.push(file);
|
|
194
|
-
} else if (handle.kind === 'directory') {
|
|
195
|
-
const dirPath = basePath + handle.name + '/';
|
|
196
|
-
for await (const entry of handle.values()) {
|
|
197
|
-
await processFileSystemHandle(entry, files, dirPath);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
161
|
+
```tsx
|
|
162
|
+
function AdvancedUploader() {
|
|
163
|
+
const drop = useDrop({ ship });
|
|
201
164
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
files
|
|
205
|
-
|
|
206
|
-
): Promise<void> => {
|
|
207
|
-
if (entry.isFile) {
|
|
208
|
-
const file = await new Promise<File>((resolve, reject) => {
|
|
209
|
-
entry.file(resolve, reject);
|
|
210
|
-
});
|
|
211
|
-
// Set webkitRelativePath for Ship SDK compatibility
|
|
212
|
-
Object.defineProperty(file, 'webkitRelativePath', {
|
|
213
|
-
value: basePath + entry.name,
|
|
214
|
-
writable: false,
|
|
215
|
-
enumerable: true,
|
|
216
|
-
configurable: true,
|
|
217
|
-
});
|
|
218
|
-
files.push(file);
|
|
219
|
-
} else if (entry.isDirectory) {
|
|
220
|
-
const dirReader = entry.createReader();
|
|
221
|
-
const entries = await new Promise<any[]>((resolve, reject) => {
|
|
222
|
-
dirReader.readEntries(resolve, reject);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
for (const childEntry of entries) {
|
|
226
|
-
await processEntry(childEntry, files, basePath + entry.name + '/');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
165
|
+
const handleManualDrop = async (e: React.DragEvent) => {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
const files = Array.from(e.dataTransfer.files);
|
|
168
|
+
await drop.processFiles(files);
|
|
229
169
|
};
|
|
230
170
|
|
|
231
171
|
return (
|
|
232
|
-
<div
|
|
233
|
-
|
|
234
|
-
onDragLeave={() => setIsDragActive(false)}
|
|
235
|
-
onDrop={handleDrop}
|
|
236
|
-
className={isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
|
|
237
|
-
>
|
|
238
|
-
{drop.isProcessing ? (
|
|
239
|
-
<p>Processing {drop.files.length} files...</p>
|
|
240
|
-
) : (
|
|
241
|
-
<p>Drag & drop files or folders here</p>
|
|
242
|
-
)}
|
|
243
|
-
|
|
244
|
-
{drop.validationError && (
|
|
245
|
-
<div className="text-red-600">{drop.validationError.details}</div>
|
|
246
|
-
)}
|
|
172
|
+
<div onDrop={handleManualDrop}>
|
|
173
|
+
{/* Custom implementation */}
|
|
247
174
|
</div>
|
|
248
175
|
);
|
|
249
176
|
}
|
|
250
177
|
```
|
|
251
178
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
- ✅ **Preserves folder structure** via `webkitRelativePath`
|
|
255
|
-
- ✅ **Uses modern File System Access API** (no permission prompts in Chrome 86+)
|
|
256
|
-
- ✅ **Fallback to webkit APIs** for broader browser support (Safari, Firefox)
|
|
257
|
-
- ✅ **You control every aspect** of the UI and UX
|
|
179
|
+
**Note:** When using manual processing, you lose automatic folder structure preservation. The built-in `getDropzoneProps()` handles `webkitGetAsEntry` API internally to preserve folder paths.
|
|
258
180
|
|
|
259
181
|
## API
|
|
260
182
|
|
|
@@ -281,19 +203,47 @@ interface DropOptions {
|
|
|
281
203
|
|
|
282
204
|
```typescript
|
|
283
205
|
interface DropReturn {
|
|
206
|
+
// State
|
|
284
207
|
/** All processed files with their status */
|
|
285
208
|
files: ProcessedFile[];
|
|
209
|
+
/** Name of the source (file/folder/ZIP) that was dropped/selected */
|
|
210
|
+
sourceName: string;
|
|
286
211
|
/** Current status text */
|
|
287
212
|
statusText: string;
|
|
288
213
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
289
214
|
isProcessing: boolean;
|
|
215
|
+
/** Whether user is currently dragging over the dropzone */
|
|
216
|
+
isDragging: boolean;
|
|
290
217
|
/** Last validation error if any */
|
|
291
218
|
validationError: ClientError | null;
|
|
292
219
|
|
|
293
|
-
|
|
220
|
+
// Primary API: Prop getters for easy integration
|
|
221
|
+
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
222
|
+
getDropzoneProps: () => {
|
|
223
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
224
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
225
|
+
onDrop: (e: React.DragEvent) => void;
|
|
226
|
+
onClick: () => void;
|
|
227
|
+
};
|
|
228
|
+
/** Get props to spread on hidden file input element */
|
|
229
|
+
getInputProps: () => {
|
|
230
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
231
|
+
type: 'file';
|
|
232
|
+
style: { display: string };
|
|
233
|
+
multiple: boolean;
|
|
234
|
+
webkitdirectory: string;
|
|
235
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Actions
|
|
239
|
+
/** Programmatically trigger file picker */
|
|
240
|
+
open: () => void;
|
|
241
|
+
/** Manually process files (for advanced usage) */
|
|
294
242
|
processFiles: (files: File[]) => Promise<void>;
|
|
295
243
|
/** Clear all files and reset state */
|
|
296
244
|
clearAll: () => void;
|
|
245
|
+
|
|
246
|
+
// Helpers
|
|
297
247
|
/** Get only valid files ready for upload */
|
|
298
248
|
getValidFiles: () => ProcessedFile[];
|
|
299
249
|
/** Update upload state for a specific file (status, progress, message) */
|
|
@@ -514,23 +464,28 @@ This package was extracted from the `web/drop` application and is purpose-built
|
|
|
514
464
|
**1. Focused on UI Concerns**
|
|
515
465
|
- ZIP extraction for user convenience
|
|
516
466
|
- File list state management for React UIs
|
|
517
|
-
-
|
|
467
|
+
- Drag & drop event handling with folder support
|
|
468
|
+
- Folder structure preservation via `webkitGetAsEntry` API
|
|
518
469
|
- Path normalization for clean URLs
|
|
519
470
|
- These are UI/UX concerns, not deployment logic
|
|
520
471
|
|
|
521
|
-
**2.
|
|
472
|
+
**2. Prop Getters Pattern (Like react-dropzone)**
|
|
473
|
+
We provide event handlers, not visual components:
|
|
474
|
+
- ✅ **`getDropzoneProps()`** - Returns drag & drop event handlers
|
|
475
|
+
- ✅ **`getInputProps()`** - Returns file input configuration
|
|
476
|
+
- ✅ **`isDragging`** - State for visual feedback
|
|
477
|
+
- ✅ **You control the DOM** - Your markup, your styling, your design system
|
|
478
|
+
|
|
479
|
+
This is the same pattern used by popular libraries like `react-dropzone`, `downshift`, and `react-table`.
|
|
480
|
+
|
|
481
|
+
**3. Loosely Coupled Integration**
|
|
522
482
|
Following industry standards (Firebase hooks, Supabase utilities), we chose:
|
|
523
|
-
- ✅ **
|
|
524
|
-
- ✅ **Simple**:
|
|
525
|
-
- ✅ **Testable**:
|
|
483
|
+
- ✅ **Ship instance as dependency**: Validates using `ship.getConfig()`
|
|
484
|
+
- ✅ **Simple output**: ProcessedFile[] can be passed directly to Ship SDK
|
|
485
|
+
- ✅ **Testable**: Easy to mock Ship SDK for testing
|
|
526
486
|
- ✅ **Flexible**: Host app controls WHEN to deploy
|
|
527
487
|
|
|
528
|
-
|
|
529
|
-
- ❌ Passing Ship SDK instance to useDrop
|
|
530
|
-
- ❌ React Context provider pattern
|
|
531
|
-
- ❌ Global configuration singleton
|
|
532
|
-
|
|
533
|
-
**3. Type System Integration**
|
|
488
|
+
**4. Type System Integration**
|
|
534
489
|
|
|
535
490
|
ProcessedFile extends StaticFile from `@shipstatic/types` - the single source of truth for Ship SDK types:
|
|
536
491
|
|
|
@@ -540,13 +495,13 @@ File[] → ProcessedFile[] (which IS StaticFile[]) → ship.deployments.create()
|
|
|
540
495
|
|
|
541
496
|
No conversion needed. ProcessedFile adds UI-specific properties (id, name, status, progress) to StaticFile's base properties (content, path, size, md5).
|
|
542
497
|
|
|
543
|
-
**
|
|
498
|
+
**5. No Visual Components**
|
|
544
499
|
|
|
545
|
-
We deliberately don't provide
|
|
546
|
-
-
|
|
547
|
-
-
|
|
548
|
-
-
|
|
549
|
-
-
|
|
500
|
+
We deliberately don't provide styled components because:
|
|
501
|
+
- Your design system is unique to your application
|
|
502
|
+
- Styling should match your brand, not our opinions
|
|
503
|
+
- Prop getters give you full control over DOM structure and CSS
|
|
504
|
+
- Similar to how `react-dropzone` works - logic without opinions
|
|
550
505
|
|
|
551
506
|
### Error Handling Philosophy
|
|
552
507
|
|
package/dist/index.cjs
CHANGED
|
@@ -11737,7 +11737,6 @@ async function extractZipToFiles(zipFile) {
|
|
|
11737
11737
|
errors.push(`Skipped invalid path: ${path}`);
|
|
11738
11738
|
continue;
|
|
11739
11739
|
}
|
|
11740
|
-
if (isJunkFile(sanitizedPath)) continue;
|
|
11741
11740
|
try {
|
|
11742
11741
|
const content = await entry.async("blob");
|
|
11743
11742
|
const mimeType = getMimeType(sanitizedPath);
|
|
@@ -11775,11 +11774,6 @@ function normalizePath(path) {
|
|
|
11775
11774
|
}
|
|
11776
11775
|
return normalized.join("/");
|
|
11777
11776
|
}
|
|
11778
|
-
function isJunkFile(path) {
|
|
11779
|
-
const basename = (path.split("/").pop() || "").toLowerCase();
|
|
11780
|
-
const junkFiles = [".ds_store", "thumbs.db", "desktop.ini", "._.ds_store"];
|
|
11781
|
-
return path.toLowerCase().startsWith("__macosx/") || junkFiles.includes(basename);
|
|
11782
|
-
}
|
|
11783
11777
|
function isZipFile(file) {
|
|
11784
11778
|
return file.type === "application/zip" || file.type === "application/x-zip-compressed" || file.name.toLowerCase().endsWith(".zip");
|
|
11785
11779
|
}
|
|
@@ -11840,6 +11834,38 @@ function stripCommonPrefix(files) {
|
|
|
11840
11834
|
path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path
|
|
11841
11835
|
}));
|
|
11842
11836
|
}
|
|
11837
|
+
async function traverseFileTree(entry, files, currentPath = "") {
|
|
11838
|
+
if (entry.isFile) {
|
|
11839
|
+
const file = await new Promise((resolve, reject) => {
|
|
11840
|
+
entry.file(resolve, reject);
|
|
11841
|
+
});
|
|
11842
|
+
const relativePath = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
11843
|
+
Object.defineProperty(file, "webkitRelativePath", {
|
|
11844
|
+
value: relativePath,
|
|
11845
|
+
writable: false
|
|
11846
|
+
});
|
|
11847
|
+
files.push(file);
|
|
11848
|
+
} else if (entry.isDirectory) {
|
|
11849
|
+
const dirReader = entry.createReader();
|
|
11850
|
+
let allEntries = [];
|
|
11851
|
+
const readEntriesBatch = async () => {
|
|
11852
|
+
const batch = await new Promise(
|
|
11853
|
+
(resolve, reject) => {
|
|
11854
|
+
dirReader.readEntries(resolve, reject);
|
|
11855
|
+
}
|
|
11856
|
+
);
|
|
11857
|
+
if (batch.length > 0) {
|
|
11858
|
+
allEntries = allEntries.concat(batch);
|
|
11859
|
+
await readEntriesBatch();
|
|
11860
|
+
}
|
|
11861
|
+
};
|
|
11862
|
+
await readEntriesBatch();
|
|
11863
|
+
for (const childEntry of allEntries) {
|
|
11864
|
+
const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
|
|
11865
|
+
await traverseFileTree(childEntry, files, entryPath);
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
}
|
|
11843
11869
|
function useDrop(options) {
|
|
11844
11870
|
const {
|
|
11845
11871
|
ship: ship$1,
|
|
@@ -11851,8 +11877,10 @@ function useDrop(options) {
|
|
|
11851
11877
|
const [sourceName, setSourceName] = react.useState("");
|
|
11852
11878
|
const [statusText, setStatusText] = react.useState("");
|
|
11853
11879
|
const [isProcessing, setIsProcessing] = react.useState(false);
|
|
11880
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
11854
11881
|
const [validationError, setValidationError] = react.useState(null);
|
|
11855
11882
|
const isProcessingRef = react.useRef(false);
|
|
11883
|
+
const inputRef = react.useRef(null);
|
|
11856
11884
|
const processFiles = react.useCallback(async (newFiles) => {
|
|
11857
11885
|
if (isProcessingRef.current) {
|
|
11858
11886
|
console.warn("File processing already in progress. Ignoring duplicate call.");
|
|
@@ -11893,9 +11921,16 @@ function useDrop(options) {
|
|
|
11893
11921
|
} else {
|
|
11894
11922
|
allFiles.push(...newFiles);
|
|
11895
11923
|
}
|
|
11924
|
+
const getFilePath = (f) => {
|
|
11925
|
+
const webkitPath = f.webkitRelativePath;
|
|
11926
|
+
return webkitPath && webkitPath.trim() ? webkitPath : f.name;
|
|
11927
|
+
};
|
|
11928
|
+
const filePaths = allFiles.map(getFilePath);
|
|
11929
|
+
const validPaths = new Set(ship.filterJunk(filePaths));
|
|
11930
|
+
const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
|
|
11896
11931
|
setStatusText("Processing files...");
|
|
11897
11932
|
const processedFiles = await Promise.all(
|
|
11898
|
-
|
|
11933
|
+
cleanFiles.map((file) => createProcessedFile(file))
|
|
11899
11934
|
);
|
|
11900
11935
|
const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
|
|
11901
11936
|
const config = await ship$1.getConfig();
|
|
@@ -11937,6 +11972,7 @@ function useDrop(options) {
|
|
|
11937
11972
|
setSourceName("");
|
|
11938
11973
|
setStatusText("");
|
|
11939
11974
|
setValidationError(null);
|
|
11975
|
+
setIsDragging(false);
|
|
11940
11976
|
isProcessingRef.current = false;
|
|
11941
11977
|
setIsProcessing(false);
|
|
11942
11978
|
}, []);
|
|
@@ -11948,14 +11984,79 @@ function useDrop(options) {
|
|
|
11948
11984
|
(file) => file.id === fileId ? { ...file, ...state } : file
|
|
11949
11985
|
));
|
|
11950
11986
|
}, []);
|
|
11987
|
+
const handleDragOver = react.useCallback((e) => {
|
|
11988
|
+
e.preventDefault();
|
|
11989
|
+
setIsDragging(true);
|
|
11990
|
+
}, []);
|
|
11991
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
11992
|
+
e.preventDefault();
|
|
11993
|
+
setIsDragging(false);
|
|
11994
|
+
}, []);
|
|
11995
|
+
const handleDrop = react.useCallback(async (e) => {
|
|
11996
|
+
e.preventDefault();
|
|
11997
|
+
setIsDragging(false);
|
|
11998
|
+
const items = Array.from(e.dataTransfer.items);
|
|
11999
|
+
const files2 = [];
|
|
12000
|
+
let hasEntries = false;
|
|
12001
|
+
for (const item of items) {
|
|
12002
|
+
if (item.kind === "file") {
|
|
12003
|
+
const entry = item.webkitGetAsEntry?.();
|
|
12004
|
+
if (entry) {
|
|
12005
|
+
hasEntries = true;
|
|
12006
|
+
await traverseFileTree(
|
|
12007
|
+
entry,
|
|
12008
|
+
files2,
|
|
12009
|
+
entry.isDirectory ? entry.name : ""
|
|
12010
|
+
);
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
if (!hasEntries && e.dataTransfer.files.length > 0) {
|
|
12015
|
+
files2.push(...Array.from(e.dataTransfer.files));
|
|
12016
|
+
}
|
|
12017
|
+
if (files2.length > 0) {
|
|
12018
|
+
await processFiles(files2);
|
|
12019
|
+
}
|
|
12020
|
+
}, [processFiles]);
|
|
12021
|
+
const handleInputChange = react.useCallback((e) => {
|
|
12022
|
+
const files2 = Array.from(e.target.files || []);
|
|
12023
|
+
if (files2.length > 0) {
|
|
12024
|
+
processFiles(files2);
|
|
12025
|
+
}
|
|
12026
|
+
}, [processFiles]);
|
|
12027
|
+
const open = react.useCallback(() => {
|
|
12028
|
+
inputRef.current?.click();
|
|
12029
|
+
}, []);
|
|
12030
|
+
const getDropzoneProps = react.useCallback(() => ({
|
|
12031
|
+
onDragOver: handleDragOver,
|
|
12032
|
+
onDragLeave: handleDragLeave,
|
|
12033
|
+
onDrop: handleDrop,
|
|
12034
|
+
onClick: open
|
|
12035
|
+
}), [handleDragOver, handleDragLeave, handleDrop, open]);
|
|
12036
|
+
const getInputProps = react.useCallback(() => ({
|
|
12037
|
+
ref: inputRef,
|
|
12038
|
+
type: "file",
|
|
12039
|
+
style: { display: "none" },
|
|
12040
|
+
multiple: true,
|
|
12041
|
+
webkitdirectory: "",
|
|
12042
|
+
onChange: handleInputChange
|
|
12043
|
+
}), [handleInputChange]);
|
|
11951
12044
|
return {
|
|
12045
|
+
// State
|
|
11952
12046
|
files,
|
|
11953
12047
|
sourceName,
|
|
11954
12048
|
statusText,
|
|
11955
12049
|
isProcessing,
|
|
12050
|
+
isDragging,
|
|
11956
12051
|
validationError,
|
|
12052
|
+
// Primary API: Prop getters
|
|
12053
|
+
getDropzoneProps,
|
|
12054
|
+
getInputProps,
|
|
12055
|
+
// Actions
|
|
12056
|
+
open,
|
|
11957
12057
|
processFiles,
|
|
11958
12058
|
clearAll,
|
|
12059
|
+
// Helpers
|
|
11959
12060
|
getValidFiles: getValidFilesCallback,
|
|
11960
12061
|
updateFileStatus
|
|
11961
12062
|
};
|
|
@@ -11989,7 +12090,6 @@ exports.createProcessedFile = createProcessedFile;
|
|
|
11989
12090
|
exports.extractZipToFiles = extractZipToFiles;
|
|
11990
12091
|
exports.formatFileSize = formatFileSize;
|
|
11991
12092
|
exports.getValidFiles = getValidFiles;
|
|
11992
|
-
exports.isJunkFile = isJunkFile;
|
|
11993
12093
|
exports.isZipFile = isZipFile;
|
|
11994
12094
|
exports.normalizePath = normalizePath;
|
|
11995
12095
|
exports.stripCommonPrefix = stripCommonPrefix;
|