@shipstatic/drop 0.1.6 → 0.1.7
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 +115 -23
- package/dist/index.cjs +82 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -9
- package/dist/index.d.ts +23 -9
- package/dist/index.js +83 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,11 +80,15 @@ function MyUploader() {
|
|
|
80
80
|
{drop.isDragging ? '📂 Drop here' : '📁 Click or drag files/folders'}
|
|
81
81
|
</div>
|
|
82
82
|
|
|
83
|
-
{/* Status */}
|
|
84
|
-
|
|
83
|
+
{/* Status - using state machine */}
|
|
84
|
+
{drop.state.status && (
|
|
85
|
+
<p>
|
|
86
|
+
<strong>{drop.state.status.title}:</strong> {drop.state.status.details}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
85
89
|
|
|
86
90
|
{/* File list */}
|
|
87
|
-
{drop.files.map(file => (
|
|
91
|
+
{drop.state.files.map(file => (
|
|
88
92
|
<div key={file.id}>
|
|
89
93
|
{file.name} - {file.status}
|
|
90
94
|
</div>
|
|
@@ -93,7 +97,7 @@ function MyUploader() {
|
|
|
93
97
|
{/* Upload button */}
|
|
94
98
|
<button
|
|
95
99
|
onClick={handleUpload}
|
|
96
|
-
disabled={drop.
|
|
100
|
+
disabled={drop.state.value !== 'ready'}
|
|
97
101
|
>
|
|
98
102
|
Upload {drop.getValidFiles().length} files
|
|
99
103
|
</button>
|
|
@@ -203,19 +207,15 @@ interface DropOptions {
|
|
|
203
207
|
|
|
204
208
|
```typescript
|
|
205
209
|
interface DropReturn {
|
|
206
|
-
// State
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** Current status text */
|
|
212
|
-
statusText: string;
|
|
210
|
+
// State machine
|
|
211
|
+
/** Current state of the drop hook */
|
|
212
|
+
state: DropState;
|
|
213
|
+
|
|
214
|
+
// Convenience getters (computed from state)
|
|
213
215
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
214
216
|
isProcessing: boolean;
|
|
215
217
|
/** Whether user is currently dragging over the dropzone */
|
|
216
218
|
isDragging: boolean;
|
|
217
|
-
/** Last validation error if any */
|
|
218
|
-
validationError: ClientError | null;
|
|
219
219
|
|
|
220
220
|
// Primary API: Prop getters for easy integration
|
|
221
221
|
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
@@ -253,19 +253,111 @@ interface DropReturn {
|
|
|
253
253
|
progress?: number;
|
|
254
254
|
}) => void;
|
|
255
255
|
}
|
|
256
|
+
|
|
257
|
+
// State machine types
|
|
258
|
+
type DropStateValue =
|
|
259
|
+
| 'idle' // The hook is ready for files
|
|
260
|
+
| 'dragging' // The user is dragging files over the dropzone
|
|
261
|
+
| 'processing' // Files are being validated and processed
|
|
262
|
+
| 'ready' // Files are valid and ready for deployment
|
|
263
|
+
| 'error'; // An error occurred during processing
|
|
264
|
+
|
|
265
|
+
interface DropState {
|
|
266
|
+
value: DropStateValue;
|
|
267
|
+
files: ProcessedFile[];
|
|
268
|
+
sourceName: string;
|
|
269
|
+
status: DropStatus | null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface DropStatus {
|
|
273
|
+
title: string;
|
|
274
|
+
details: string;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## State Machine
|
|
279
|
+
|
|
280
|
+
The drop hook uses a state machine for predictable, clear state management. Instead of multiple boolean flags, you have a single `state.value` that represents exactly what's happening.
|
|
281
|
+
|
|
282
|
+
### State Flow
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
idle → dragging → idle (drag leave without drop)
|
|
286
|
+
idle → dragging → processing → ready (successful)
|
|
287
|
+
idle → dragging → processing → error (failed)
|
|
288
|
+
ready → dragging → processing → ... (new drop)
|
|
289
|
+
error → dragging → processing → ... (retry)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Using the State Machine
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
function StatusIndicator({ drop }) {
|
|
296
|
+
const { state } = drop;
|
|
297
|
+
|
|
298
|
+
switch (state.value) {
|
|
299
|
+
case 'idle':
|
|
300
|
+
return <p>Drop files here or click to select</p>;
|
|
301
|
+
|
|
302
|
+
case 'dragging':
|
|
303
|
+
return <p>Drop your files now!</p>;
|
|
304
|
+
|
|
305
|
+
case 'processing':
|
|
306
|
+
return <p>{state.status?.details || 'Processing...'}</p>;
|
|
307
|
+
|
|
308
|
+
case 'ready':
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
<p>✓ {state.files.length} files ready</p>
|
|
312
|
+
<button>Upload to Ship</button>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
case 'error':
|
|
317
|
+
return (
|
|
318
|
+
<div>
|
|
319
|
+
<p>✗ {state.status?.title}</p>
|
|
320
|
+
<p>{state.status?.details}</p>
|
|
321
|
+
<button onClick={drop.clearAll}>Try Again</button>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
256
326
|
```
|
|
257
327
|
|
|
328
|
+
### Convenience Getters
|
|
329
|
+
|
|
330
|
+
For simpler use cases, boolean convenience getters are provided:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// These are computed from state.value (read-only projections)
|
|
334
|
+
drop.isProcessing // true when state.value === 'processing'
|
|
335
|
+
drop.isDragging // true when state.value === 'dragging'
|
|
336
|
+
|
|
337
|
+
// For error information, use the state object
|
|
338
|
+
drop.state.value === 'error' // Check if in error state
|
|
339
|
+
drop.state.status?.title // Error title
|
|
340
|
+
drop.state.status?.details // Error details
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Benefits
|
|
344
|
+
|
|
345
|
+
- **No impossible states** - Can't be `isProcessing=true` AND `isDragging=true`
|
|
346
|
+
- **Clear transitions** - State flow is explicit and predictable
|
|
347
|
+
- **Better TypeScript** - Discriminated unions provide type safety
|
|
348
|
+
- **Easier debugging** - Single source of truth for what's happening
|
|
349
|
+
|
|
258
350
|
## Error Handling
|
|
259
351
|
|
|
260
352
|
### Per-File Error Display
|
|
261
353
|
|
|
262
|
-
Each file in the `files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
354
|
+
Each file in the `state.files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
263
355
|
|
|
264
356
|
```tsx
|
|
265
357
|
function FileList({ drop }) {
|
|
266
358
|
return (
|
|
267
359
|
<div>
|
|
268
|
-
{drop.files.map(file => (
|
|
360
|
+
{drop.state.files.map(file => (
|
|
269
361
|
<div key={file.id}>
|
|
270
362
|
<span>{file.path}</span>
|
|
271
363
|
|
|
@@ -282,7 +374,7 @@ function FileList({ drop }) {
|
|
|
282
374
|
))}
|
|
283
375
|
|
|
284
376
|
{/* If validation fails, allow user to clear all and try again */}
|
|
285
|
-
{drop.
|
|
377
|
+
{drop.state.value === 'error' && (
|
|
286
378
|
<button onClick={drop.clearAll}>
|
|
287
379
|
Clear All & Try Again
|
|
288
380
|
</button>
|
|
@@ -298,15 +390,15 @@ function FileList({ drop }) {
|
|
|
298
390
|
- `empty_file` - File is 0 bytes
|
|
299
391
|
- `ready` - File passed all validation and is ready for upload
|
|
300
392
|
|
|
301
|
-
###
|
|
393
|
+
### Error State Summary
|
|
302
394
|
|
|
303
|
-
|
|
395
|
+
When files fail validation or processing, check the error state:
|
|
304
396
|
|
|
305
397
|
```tsx
|
|
306
|
-
{drop.
|
|
398
|
+
{drop.state.value === 'error' && drop.state.status && (
|
|
307
399
|
<div>
|
|
308
|
-
<p>{drop.
|
|
309
|
-
<p>{drop.
|
|
400
|
+
<p>{drop.state.status.title}</p>
|
|
401
|
+
<p>{drop.state.status.details}</p>
|
|
310
402
|
</div>
|
|
311
403
|
)}
|
|
312
404
|
```
|
|
@@ -336,10 +428,10 @@ Use `clearAll()` to reset and try again:
|
|
|
336
428
|
|
|
337
429
|
```tsx
|
|
338
430
|
// If validation fails, show user which files failed
|
|
339
|
-
{drop.
|
|
431
|
+
{drop.state.value === 'error' && (
|
|
340
432
|
<div>
|
|
341
433
|
<p>Validation failed. Please fix the issues and try again:</p>
|
|
342
|
-
{drop.files.map(file => (
|
|
434
|
+
{drop.state.files.map(file => (
|
|
343
435
|
<div key={file.id}>
|
|
344
436
|
{file.path}: {file.statusMessage}
|
|
345
437
|
</div>
|
package/dist/index.cjs
CHANGED
|
@@ -11873,28 +11873,32 @@ function useDrop(options) {
|
|
|
11873
11873
|
onFilesReady,
|
|
11874
11874
|
stripPrefix = true
|
|
11875
11875
|
} = options;
|
|
11876
|
-
const
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11876
|
+
const initialState = {
|
|
11877
|
+
value: "idle",
|
|
11878
|
+
files: [],
|
|
11879
|
+
sourceName: "",
|
|
11880
|
+
status: null
|
|
11881
|
+
};
|
|
11882
|
+
const [state, setState] = react.useState(initialState);
|
|
11882
11883
|
const isProcessingRef = react.useRef(false);
|
|
11883
11884
|
const inputRef = react.useRef(null);
|
|
11885
|
+
const isProcessing = react.useMemo(() => state.value === "processing", [state.value]);
|
|
11886
|
+
const isDragging = react.useMemo(() => state.value === "dragging", [state.value]);
|
|
11884
11887
|
const processFiles = react.useCallback(async (newFiles) => {
|
|
11885
11888
|
if (isProcessingRef.current) {
|
|
11886
11889
|
console.warn("File processing already in progress. Ignoring duplicate call.");
|
|
11887
11890
|
return;
|
|
11888
11891
|
}
|
|
11889
11892
|
if (!newFiles || newFiles.length === 0) {
|
|
11890
|
-
setStatusText("No files selected.");
|
|
11891
11893
|
return;
|
|
11892
11894
|
}
|
|
11893
11895
|
isProcessingRef.current = true;
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11896
|
+
setState({
|
|
11897
|
+
value: "processing",
|
|
11898
|
+
files: [],
|
|
11899
|
+
sourceName: "",
|
|
11900
|
+
status: { title: "Processing...", details: "Validating and preparing files." }
|
|
11901
|
+
});
|
|
11898
11902
|
try {
|
|
11899
11903
|
let detectedSourceName = "";
|
|
11900
11904
|
if (newFiles.length === 1 && isZipFile(newFiles[0])) {
|
|
@@ -11907,12 +11911,14 @@ function useDrop(options) {
|
|
|
11907
11911
|
detectedSourceName = newFiles[0].name;
|
|
11908
11912
|
}
|
|
11909
11913
|
}
|
|
11910
|
-
setSourceName(detectedSourceName);
|
|
11911
11914
|
const allFiles = [];
|
|
11912
11915
|
const shouldExtractZip = newFiles.length === 1 && isZipFile(newFiles[0]);
|
|
11913
11916
|
if (shouldExtractZip) {
|
|
11914
11917
|
const zipFile = newFiles[0];
|
|
11915
|
-
|
|
11918
|
+
setState((prev) => ({
|
|
11919
|
+
...prev,
|
|
11920
|
+
status: { title: "Extracting...", details: `Extracting ${zipFile.name}...` }
|
|
11921
|
+
}));
|
|
11916
11922
|
const { files: extractedFiles, errors } = await extractZipToFiles(zipFile);
|
|
11917
11923
|
if (errors.length > 0) {
|
|
11918
11924
|
console.warn("ZIP extraction errors:", errors);
|
|
@@ -11928,29 +11934,44 @@ function useDrop(options) {
|
|
|
11928
11934
|
const filePaths = allFiles.map(getFilePath);
|
|
11929
11935
|
const validPaths = new Set(ship.filterJunk(filePaths));
|
|
11930
11936
|
const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
|
|
11931
|
-
|
|
11937
|
+
setState((prev) => ({
|
|
11938
|
+
...prev,
|
|
11939
|
+
status: { title: "Processing...", details: "Processing files..." }
|
|
11940
|
+
}));
|
|
11932
11941
|
const processedFiles = await Promise.all(
|
|
11933
11942
|
cleanFiles.map((file) => createProcessedFile(file))
|
|
11934
11943
|
);
|
|
11935
11944
|
const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
|
|
11936
11945
|
const config = await ship$1.getConfig();
|
|
11937
11946
|
const validation = ship.validateFiles(finalFiles, config);
|
|
11938
|
-
setFiles(validation.files);
|
|
11939
|
-
setValidationError(validation.error);
|
|
11940
11947
|
if (validation.error) {
|
|
11941
|
-
|
|
11948
|
+
setState({
|
|
11949
|
+
value: "error",
|
|
11950
|
+
files: validation.files,
|
|
11951
|
+
sourceName: detectedSourceName,
|
|
11952
|
+
status: { title: validation.error.error, details: validation.error.details }
|
|
11953
|
+
});
|
|
11942
11954
|
onValidationError?.(validation.error);
|
|
11943
11955
|
} else if (validation.validFiles.length > 0) {
|
|
11944
|
-
|
|
11956
|
+
setState({
|
|
11957
|
+
value: "ready",
|
|
11958
|
+
files: validation.files,
|
|
11959
|
+
sourceName: detectedSourceName,
|
|
11960
|
+
status: { title: "Ready", details: `${validation.validFiles.length} file(s) are ready.` }
|
|
11961
|
+
});
|
|
11945
11962
|
onFilesReady?.(validation.validFiles);
|
|
11946
11963
|
} else {
|
|
11947
11964
|
const noValidError = {
|
|
11948
11965
|
error: "No Valid Files",
|
|
11949
|
-
details: "
|
|
11966
|
+
details: "None of the provided files could be processed.",
|
|
11950
11967
|
isClientError: true
|
|
11951
11968
|
};
|
|
11952
|
-
|
|
11953
|
-
|
|
11969
|
+
setState({
|
|
11970
|
+
value: "error",
|
|
11971
|
+
files: validation.files,
|
|
11972
|
+
sourceName: detectedSourceName,
|
|
11973
|
+
status: { title: noValidError.error, details: noValidError.details }
|
|
11974
|
+
});
|
|
11954
11975
|
onValidationError?.(noValidError);
|
|
11955
11976
|
}
|
|
11956
11977
|
} catch (error) {
|
|
@@ -11959,44 +11980,52 @@ function useDrop(options) {
|
|
|
11959
11980
|
details: `Failed to process files: ${error instanceof Error ? error.message : String(error)}`,
|
|
11960
11981
|
isClientError: true
|
|
11961
11982
|
};
|
|
11962
|
-
|
|
11963
|
-
|
|
11983
|
+
setState((prev) => ({
|
|
11984
|
+
...prev,
|
|
11985
|
+
value: "error",
|
|
11986
|
+
status: { title: processingError.error, details: processingError.details }
|
|
11987
|
+
}));
|
|
11964
11988
|
onValidationError?.(processingError);
|
|
11965
11989
|
} finally {
|
|
11966
11990
|
isProcessingRef.current = false;
|
|
11967
|
-
setIsProcessing(false);
|
|
11968
11991
|
}
|
|
11969
11992
|
}, [ship$1, onValidationError, onFilesReady, stripPrefix]);
|
|
11970
11993
|
const clearAll = react.useCallback(() => {
|
|
11971
|
-
|
|
11972
|
-
setSourceName("");
|
|
11973
|
-
setStatusText("");
|
|
11974
|
-
setValidationError(null);
|
|
11975
|
-
setIsDragging(false);
|
|
11994
|
+
setState(initialState);
|
|
11976
11995
|
isProcessingRef.current = false;
|
|
11977
|
-
setIsProcessing(false);
|
|
11978
11996
|
}, []);
|
|
11979
11997
|
const getValidFilesCallback = react.useCallback(() => {
|
|
11980
|
-
return getValidFiles(files);
|
|
11981
|
-
}, [files]);
|
|
11982
|
-
const updateFileStatus = react.useCallback((fileId,
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11998
|
+
return getValidFiles(state.files);
|
|
11999
|
+
}, [state.files]);
|
|
12000
|
+
const updateFileStatus = react.useCallback((fileId, fileState) => {
|
|
12001
|
+
setState((prev) => ({
|
|
12002
|
+
...prev,
|
|
12003
|
+
files: prev.files.map(
|
|
12004
|
+
(file) => file.id === fileId ? { ...file, ...fileState } : file
|
|
12005
|
+
)
|
|
12006
|
+
}));
|
|
11986
12007
|
}, []);
|
|
11987
12008
|
const handleDragOver = react.useCallback((e) => {
|
|
11988
12009
|
e.preventDefault();
|
|
11989
|
-
|
|
12010
|
+
setState((prev) => {
|
|
12011
|
+
if (prev.value === "idle" || prev.value === "ready" || prev.value === "error") {
|
|
12012
|
+
return { ...prev, value: "dragging" };
|
|
12013
|
+
}
|
|
12014
|
+
return prev;
|
|
12015
|
+
});
|
|
11990
12016
|
}, []);
|
|
11991
12017
|
const handleDragLeave = react.useCallback((e) => {
|
|
11992
12018
|
e.preventDefault();
|
|
11993
|
-
|
|
12019
|
+
setState((prev) => {
|
|
12020
|
+
if (prev.value !== "dragging") return prev;
|
|
12021
|
+
const nextValue = prev.files.length > 0 ? prev.status?.title === "Ready" ? "ready" : "error" : "idle";
|
|
12022
|
+
return { ...prev, value: nextValue };
|
|
12023
|
+
});
|
|
11994
12024
|
}, []);
|
|
11995
12025
|
const handleDrop = react.useCallback(async (e) => {
|
|
11996
12026
|
e.preventDefault();
|
|
11997
|
-
setIsDragging(false);
|
|
11998
12027
|
const items = Array.from(e.dataTransfer.items);
|
|
11999
|
-
const
|
|
12028
|
+
const files = [];
|
|
12000
12029
|
let hasEntries = false;
|
|
12001
12030
|
for (const item of items) {
|
|
12002
12031
|
if (item.kind === "file") {
|
|
@@ -12005,23 +12034,25 @@ function useDrop(options) {
|
|
|
12005
12034
|
hasEntries = true;
|
|
12006
12035
|
await traverseFileTree(
|
|
12007
12036
|
entry,
|
|
12008
|
-
|
|
12037
|
+
files,
|
|
12009
12038
|
entry.isDirectory ? entry.name : ""
|
|
12010
12039
|
);
|
|
12011
12040
|
}
|
|
12012
12041
|
}
|
|
12013
12042
|
}
|
|
12014
12043
|
if (!hasEntries && e.dataTransfer.files.length > 0) {
|
|
12015
|
-
|
|
12044
|
+
files.push(...Array.from(e.dataTransfer.files));
|
|
12016
12045
|
}
|
|
12017
|
-
if (
|
|
12018
|
-
await processFiles(
|
|
12046
|
+
if (files.length > 0) {
|
|
12047
|
+
await processFiles(files);
|
|
12048
|
+
} else if (state.value === "dragging") {
|
|
12049
|
+
setState((prev) => ({ ...prev, value: "idle" }));
|
|
12019
12050
|
}
|
|
12020
|
-
}, [processFiles]);
|
|
12051
|
+
}, [processFiles, state.value]);
|
|
12021
12052
|
const handleInputChange = react.useCallback((e) => {
|
|
12022
|
-
const
|
|
12023
|
-
if (
|
|
12024
|
-
processFiles(
|
|
12053
|
+
const files = Array.from(e.target.files || []);
|
|
12054
|
+
if (files.length > 0) {
|
|
12055
|
+
processFiles(files);
|
|
12025
12056
|
}
|
|
12026
12057
|
}, [processFiles]);
|
|
12027
12058
|
const open = react.useCallback(() => {
|
|
@@ -12042,13 +12073,11 @@ function useDrop(options) {
|
|
|
12042
12073
|
onChange: handleInputChange
|
|
12043
12074
|
}), [handleInputChange]);
|
|
12044
12075
|
return {
|
|
12045
|
-
// State
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
statusText,
|
|
12076
|
+
// State machine
|
|
12077
|
+
state,
|
|
12078
|
+
// Convenience getters (computed from state)
|
|
12049
12079
|
isProcessing,
|
|
12050
12080
|
isDragging,
|
|
12051
|
-
validationError,
|
|
12052
12081
|
// Primary API: Prop getters
|
|
12053
12082
|
getDropzoneProps,
|
|
12054
12083
|
getInputProps,
|