@uploadista/vue 0.0.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/.turbo/turbo-check.log +240 -0
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/package.json +36 -0
- package/src/components/FlowUploadList.vue +342 -0
- package/src/components/FlowUploadZone.vue +305 -0
- package/src/components/UploadList.vue +303 -0
- package/src/components/UploadZone.vue +254 -0
- package/src/components/index.ts +11 -0
- package/src/composables/index.ts +44 -0
- package/src/composables/plugin.ts +76 -0
- package/src/composables/useDragDrop.ts +343 -0
- package/src/composables/useFlowUpload.ts +431 -0
- package/src/composables/useMultiFlowUpload.ts +322 -0
- package/src/composables/useMultiUpload.ts +546 -0
- package/src/composables/useUpload.ts +300 -0
- package/src/composables/useUploadMetrics.ts +502 -0
- package/src/composables/useUploadistaClient.ts +73 -0
- package/src/index.ts +28 -0
- package/src/providers/UploadistaProvider.vue +69 -0
- package/src/providers/index.ts +1 -0
- package/src/utils/index.ts +156 -0
- package/src/utils/is-browser-file.ts +2 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowUploadConfig,
|
|
3
|
+
UploadistaEvent,
|
|
4
|
+
} from "@uploadista/client-browser";
|
|
5
|
+
import { EventType, type FlowEvent } from "@uploadista/core/flow";
|
|
6
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
7
|
+
import { UploadEventType } from "@uploadista/core/types";
|
|
8
|
+
import { computed, onUnmounted, readonly, ref } from "vue";
|
|
9
|
+
import { useUploadistaClient } from "./useUploadistaClient";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if an event is a flow event
|
|
13
|
+
*/
|
|
14
|
+
function isFlowEvent(event: UploadistaEvent): event is FlowEvent {
|
|
15
|
+
const flowEvent = event as FlowEvent;
|
|
16
|
+
return (
|
|
17
|
+
flowEvent.eventType === EventType.FlowStart ||
|
|
18
|
+
flowEvent.eventType === EventType.FlowEnd ||
|
|
19
|
+
flowEvent.eventType === EventType.FlowError ||
|
|
20
|
+
flowEvent.eventType === EventType.NodeStart ||
|
|
21
|
+
flowEvent.eventType === EventType.NodeEnd ||
|
|
22
|
+
flowEvent.eventType === EventType.NodePause ||
|
|
23
|
+
flowEvent.eventType === EventType.NodeResume ||
|
|
24
|
+
flowEvent.eventType === EventType.NodeError
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type FlowUploadStatus =
|
|
29
|
+
| "idle"
|
|
30
|
+
| "uploading"
|
|
31
|
+
| "processing"
|
|
32
|
+
| "success"
|
|
33
|
+
| "error"
|
|
34
|
+
| "aborted";
|
|
35
|
+
|
|
36
|
+
export interface FlowUploadState<TOutput = UploadFile> {
|
|
37
|
+
status: FlowUploadStatus;
|
|
38
|
+
progress: number;
|
|
39
|
+
bytesUploaded: number;
|
|
40
|
+
totalBytes: number | null;
|
|
41
|
+
error: Error | null;
|
|
42
|
+
result: TOutput | null;
|
|
43
|
+
jobId: string | null;
|
|
44
|
+
// Flow execution tracking
|
|
45
|
+
flowStarted: boolean;
|
|
46
|
+
currentNodeName: string | null;
|
|
47
|
+
currentNodeType: string | null;
|
|
48
|
+
// Full flow outputs (all output nodes)
|
|
49
|
+
flowOutputs: Record<string, unknown> | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UseFlowUploadOptions<TOutput = UploadFile> {
|
|
53
|
+
/**
|
|
54
|
+
* Flow configuration
|
|
55
|
+
*/
|
|
56
|
+
flowConfig: FlowUploadConfig;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called when upload progress updates
|
|
60
|
+
*/
|
|
61
|
+
onProgress?: (
|
|
62
|
+
progress: number,
|
|
63
|
+
bytesUploaded: number,
|
|
64
|
+
totalBytes: number | null,
|
|
65
|
+
) => void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Called when a chunk completes
|
|
69
|
+
*/
|
|
70
|
+
onChunkComplete?: (
|
|
71
|
+
chunkSize: number,
|
|
72
|
+
bytesAccepted: number,
|
|
73
|
+
bytesTotal: number | null,
|
|
74
|
+
) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Called when the flow completes successfully (receives full flow outputs)
|
|
78
|
+
* This is the recommended callback for multi-output flows
|
|
79
|
+
* Format: { [outputNodeId]: result, ... }
|
|
80
|
+
*/
|
|
81
|
+
onFlowComplete?: (outputs: Record<string, unknown>) => void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Called when upload succeeds (legacy, single-output flows)
|
|
85
|
+
* For single-output flows, receives the value from the specified outputNodeId
|
|
86
|
+
* or the first output node if outputNodeId is not specified
|
|
87
|
+
*/
|
|
88
|
+
onSuccess?: (result: TOutput) => void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called when upload fails
|
|
92
|
+
*/
|
|
93
|
+
onError?: (error: Error) => void;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Called when upload is aborted
|
|
97
|
+
*/
|
|
98
|
+
onAbort?: () => void;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Custom retry logic
|
|
102
|
+
*/
|
|
103
|
+
onShouldRetry?: (error: Error, retryAttempt: number) => boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const initialState: FlowUploadState = {
|
|
107
|
+
status: "idle",
|
|
108
|
+
progress: 0,
|
|
109
|
+
bytesUploaded: 0,
|
|
110
|
+
totalBytes: null,
|
|
111
|
+
error: null,
|
|
112
|
+
result: null,
|
|
113
|
+
jobId: null,
|
|
114
|
+
flowStarted: false,
|
|
115
|
+
currentNodeName: null,
|
|
116
|
+
currentNodeType: null,
|
|
117
|
+
flowOutputs: null,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Vue composable for uploading files through a flow.
|
|
122
|
+
*
|
|
123
|
+
* This composable provides a simple interface for uploading files through a flow.
|
|
124
|
+
* The flow handles the upload process and can perform post-processing like
|
|
125
|
+
* saving to storage, optimizing images, etc.
|
|
126
|
+
*
|
|
127
|
+
* Must be used within a component tree that has the Uploadista plugin installed.
|
|
128
|
+
* Events are automatically wired up through the plugin.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```vue
|
|
132
|
+
* <script setup lang="ts">
|
|
133
|
+
* import { useFlowUpload } from '@uploadista/vue';
|
|
134
|
+
*
|
|
135
|
+
* const flowUpload = useFlowUpload({
|
|
136
|
+
* flowConfig: {
|
|
137
|
+
* flowId: "my-upload-flow",
|
|
138
|
+
* storageId: "my-storage",
|
|
139
|
+
* },
|
|
140
|
+
* onSuccess: (result) => {
|
|
141
|
+
* console.log("Upload complete:", result);
|
|
142
|
+
* },
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* const handleFileChange = (event: Event) => {
|
|
146
|
+
* const file = (event.target as HTMLInputElement).files?.[0];
|
|
147
|
+
* if (file) flowUpload.upload(file);
|
|
148
|
+
* };
|
|
149
|
+
* </script>
|
|
150
|
+
*
|
|
151
|
+
* <template>
|
|
152
|
+
* <input type="file" @change="handleFileChange" />
|
|
153
|
+
* </template>
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function useFlowUpload<TOutput = UploadFile>(
|
|
157
|
+
options: UseFlowUploadOptions<TOutput>,
|
|
158
|
+
) {
|
|
159
|
+
// Get client and event subscription
|
|
160
|
+
const client = useUploadistaClient();
|
|
161
|
+
const state = ref<FlowUploadState<TOutput>>(
|
|
162
|
+
initialState as FlowUploadState<TOutput>,
|
|
163
|
+
);
|
|
164
|
+
const abortFn = ref<(() => void) | null>(null);
|
|
165
|
+
const jobId = ref<string | null>(null);
|
|
166
|
+
|
|
167
|
+
// Handle flow events
|
|
168
|
+
const handleFlowEvent = (event: FlowEvent) => {
|
|
169
|
+
console.log("handleFlowEvent", event);
|
|
170
|
+
// Only handle events for the current job
|
|
171
|
+
if (!jobId.value || event.jobId !== jobId.value) {
|
|
172
|
+
console.log("handleFlowEvent - jobId mismatch", event.jobId, jobId.value);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
switch (event.eventType) {
|
|
177
|
+
case EventType.FlowStart:
|
|
178
|
+
state.value = {
|
|
179
|
+
...state.value,
|
|
180
|
+
flowStarted: true,
|
|
181
|
+
status: "processing",
|
|
182
|
+
};
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case EventType.NodeStart:
|
|
186
|
+
state.value = {
|
|
187
|
+
...state.value,
|
|
188
|
+
status: "processing",
|
|
189
|
+
currentNodeName: event.nodeName,
|
|
190
|
+
currentNodeType: event.nodeType,
|
|
191
|
+
};
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case EventType.NodePause:
|
|
195
|
+
// When input node pauses, it's waiting for upload - switch to uploading state
|
|
196
|
+
state.value = {
|
|
197
|
+
...state.value,
|
|
198
|
+
status: "uploading",
|
|
199
|
+
currentNodeName: event.nodeName,
|
|
200
|
+
};
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case EventType.NodeResume:
|
|
204
|
+
// When node resumes, upload is complete - switch to processing state
|
|
205
|
+
state.value = {
|
|
206
|
+
...state.value,
|
|
207
|
+
status: "processing",
|
|
208
|
+
currentNodeName: event.nodeName,
|
|
209
|
+
currentNodeType: event.nodeType,
|
|
210
|
+
};
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case EventType.NodeEnd:
|
|
214
|
+
state.value = {
|
|
215
|
+
...state.value,
|
|
216
|
+
status:
|
|
217
|
+
state.value.status === "uploading"
|
|
218
|
+
? "processing"
|
|
219
|
+
: state.value.status,
|
|
220
|
+
currentNodeName: null,
|
|
221
|
+
currentNodeType: null,
|
|
222
|
+
};
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case EventType.FlowEnd: {
|
|
226
|
+
// Get flow outputs from the event result
|
|
227
|
+
const flowOutputs = (event.result as Record<string, unknown>) || null;
|
|
228
|
+
|
|
229
|
+
// Call onFlowComplete with full outputs
|
|
230
|
+
if (flowOutputs && options.onFlowComplete) {
|
|
231
|
+
options.onFlowComplete(flowOutputs);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extract single output for onSuccess callback
|
|
235
|
+
let extractedOutput: TOutput | null = null;
|
|
236
|
+
if (flowOutputs) {
|
|
237
|
+
if (
|
|
238
|
+
options.flowConfig.outputNodeId &&
|
|
239
|
+
options.flowConfig.outputNodeId in flowOutputs
|
|
240
|
+
) {
|
|
241
|
+
// Use specified output node
|
|
242
|
+
extractedOutput = flowOutputs[
|
|
243
|
+
options.flowConfig.outputNodeId
|
|
244
|
+
] as TOutput;
|
|
245
|
+
} else {
|
|
246
|
+
// Use first output node
|
|
247
|
+
const firstOutputValue = Object.values(flowOutputs)[0];
|
|
248
|
+
extractedOutput = firstOutputValue as TOutput;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Call onSuccess with extracted output
|
|
253
|
+
if (extractedOutput && options.onSuccess) {
|
|
254
|
+
options.onSuccess(extractedOutput);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
state.value = {
|
|
258
|
+
...state.value,
|
|
259
|
+
status: "success",
|
|
260
|
+
currentNodeName: null,
|
|
261
|
+
currentNodeType: null,
|
|
262
|
+
result: extractedOutput,
|
|
263
|
+
flowOutputs,
|
|
264
|
+
};
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case EventType.FlowError:
|
|
269
|
+
state.value = {
|
|
270
|
+
...state.value,
|
|
271
|
+
status: "error",
|
|
272
|
+
error: new Error(event.error),
|
|
273
|
+
};
|
|
274
|
+
options.onError?.(new Error(event.error));
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case EventType.NodeError:
|
|
278
|
+
state.value = {
|
|
279
|
+
...state.value,
|
|
280
|
+
status: "error",
|
|
281
|
+
error: new Error(event.error),
|
|
282
|
+
};
|
|
283
|
+
options.onError?.(new Error(event.error));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Automatically subscribe to flow events and upload events
|
|
289
|
+
const unsubscribe = client.subscribeToEvents((event: UploadistaEvent) => {
|
|
290
|
+
console.log("subscribeToEvents", event);
|
|
291
|
+
// Handle flow events
|
|
292
|
+
if (isFlowEvent(event)) {
|
|
293
|
+
handleFlowEvent(event);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Handle upload progress events for this job's upload
|
|
298
|
+
const uploadEvent = event as {
|
|
299
|
+
type: string;
|
|
300
|
+
data?: { id: string; progress: number; total: number };
|
|
301
|
+
flow?: { jobId: string };
|
|
302
|
+
};
|
|
303
|
+
if (
|
|
304
|
+
uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
|
|
305
|
+
uploadEvent.flow?.jobId === jobId.value &&
|
|
306
|
+
uploadEvent.data
|
|
307
|
+
) {
|
|
308
|
+
const { progress: bytesUploaded, total: totalBytes } = uploadEvent.data;
|
|
309
|
+
const progress = totalBytes
|
|
310
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
311
|
+
: 0;
|
|
312
|
+
|
|
313
|
+
state.value = {
|
|
314
|
+
...state.value,
|
|
315
|
+
progress,
|
|
316
|
+
bytesUploaded,
|
|
317
|
+
totalBytes,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Cleanup on unmount
|
|
323
|
+
onUnmounted(() => {
|
|
324
|
+
unsubscribe();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const abort = () => {
|
|
328
|
+
if (abortFn.value) {
|
|
329
|
+
abortFn.value();
|
|
330
|
+
abortFn.value = null;
|
|
331
|
+
|
|
332
|
+
state.value = {
|
|
333
|
+
...state.value,
|
|
334
|
+
status: "aborted",
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
options.onAbort?.();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const upload = async (file: File | Blob) => {
|
|
342
|
+
jobId.value = null;
|
|
343
|
+
|
|
344
|
+
state.value = {
|
|
345
|
+
...initialState,
|
|
346
|
+
status: "uploading",
|
|
347
|
+
totalBytes: file.size,
|
|
348
|
+
} as FlowUploadState<TOutput>;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const { abort: _abortFn } = await client.client.uploadWithFlow(
|
|
352
|
+
file,
|
|
353
|
+
options.flowConfig,
|
|
354
|
+
{
|
|
355
|
+
onJobStart: (id: string) => {
|
|
356
|
+
jobId.value = id;
|
|
357
|
+
state.value = { ...state.value, jobId: id };
|
|
358
|
+
},
|
|
359
|
+
onProgress: (
|
|
360
|
+
_uploadId: string,
|
|
361
|
+
bytesUploaded: number,
|
|
362
|
+
totalBytes: number | null,
|
|
363
|
+
) => {
|
|
364
|
+
const progress = totalBytes
|
|
365
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
366
|
+
: 0;
|
|
367
|
+
|
|
368
|
+
state.value = {
|
|
369
|
+
...state.value,
|
|
370
|
+
progress,
|
|
371
|
+
bytesUploaded,
|
|
372
|
+
totalBytes,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
options.onProgress?.(progress, bytesUploaded, totalBytes);
|
|
376
|
+
},
|
|
377
|
+
onChunkComplete: options.onChunkComplete,
|
|
378
|
+
onSuccess: (_result: UploadFile) => {
|
|
379
|
+
// Upload phase is complete, now waiting for flow execution
|
|
380
|
+
// Status transition from "uploading" to "processing" is handled by NodeResume event
|
|
381
|
+
state.value = {
|
|
382
|
+
...state.value,
|
|
383
|
+
progress: 100,
|
|
384
|
+
};
|
|
385
|
+
// Don't call onSuccess here - wait for FlowEnd event
|
|
386
|
+
},
|
|
387
|
+
onError: (error: Error) => {
|
|
388
|
+
state.value = {
|
|
389
|
+
...state.value,
|
|
390
|
+
status: "error",
|
|
391
|
+
error,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
options.onError?.(error);
|
|
395
|
+
},
|
|
396
|
+
onShouldRetry: options.onShouldRetry,
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
abortFn.value = _abortFn;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
state.value = {
|
|
403
|
+
...state.value,
|
|
404
|
+
status: "error",
|
|
405
|
+
error: error as Error,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
options.onError?.(error as Error);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const reset = () => {
|
|
413
|
+
state.value = initialState as FlowUploadState<TOutput>;
|
|
414
|
+
abortFn.value = null;
|
|
415
|
+
jobId.value = null;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
state: readonly(state),
|
|
420
|
+
upload,
|
|
421
|
+
abort,
|
|
422
|
+
reset,
|
|
423
|
+
isUploading: computed(
|
|
424
|
+
() =>
|
|
425
|
+
state.value.status === "uploading" ||
|
|
426
|
+
state.value.status === "processing",
|
|
427
|
+
),
|
|
428
|
+
isUploadingFile: computed(() => state.value.status === "uploading"),
|
|
429
|
+
isProcessing: computed(() => state.value.status === "processing"),
|
|
430
|
+
};
|
|
431
|
+
}
|