@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,342 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FlowUploadList - Display list of flow uploads with processing status
|
|
4
|
+
*
|
|
5
|
+
* Shows the progress and processing status of files being uploaded through flow pipelines.
|
|
6
|
+
* Supports filtering, sorting, and status-based grouping. Provides flexible slot-based
|
|
7
|
+
* customization for rendering each flow upload item.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic flow upload list
|
|
12
|
+
* <FlowUploadList :uploads="flowUploads" />
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Custom item rendering with flow status
|
|
16
|
+
* <FlowUploadList :uploads="flowUploads">
|
|
17
|
+
* <template #item="{ item, isSuccess, isError, isUploading }">
|
|
18
|
+
* <div class="flow-item">
|
|
19
|
+
* <span>{{ item.filename }}</span>
|
|
20
|
+
* <progress :value="item.uploadProgress" max="100"></progress>
|
|
21
|
+
* <span v-if="isUploading">Processing...</span>
|
|
22
|
+
* <span v-else-if="isSuccess">Complete</span>
|
|
23
|
+
* <span v-else-if="isError">Failed</span>
|
|
24
|
+
* </div>
|
|
25
|
+
* </template>
|
|
26
|
+
* </FlowUploadList>
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // With status grouping
|
|
30
|
+
* <FlowUploadList :uploads="flowUploads">
|
|
31
|
+
* <template #default="{ itemsByStatus }">
|
|
32
|
+
* <div v-if="itemsByStatus.uploading.length">
|
|
33
|
+
* <h3>Uploading...</h3>
|
|
34
|
+
* <div v-for="item of itemsByStatus.uploading" :key="item.id">
|
|
35
|
+
* {{ item.filename }}
|
|
36
|
+
* </div>
|
|
37
|
+
* </div>
|
|
38
|
+
* </template>
|
|
39
|
+
* </FlowUploadList>
|
|
40
|
+
*/
|
|
41
|
+
import type {
|
|
42
|
+
BrowserUploadInput,
|
|
43
|
+
FlowUploadItem,
|
|
44
|
+
} from "@uploadista/client-browser";
|
|
45
|
+
import { computed } from "vue";
|
|
46
|
+
import { isBrowserFile } from "../utils";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Props for the FlowUploadList component
|
|
50
|
+
* @property {FlowUploadItem[]} uploads - Array of flow upload items to display
|
|
51
|
+
* @property {Function} filter - Optional filter for which items to display
|
|
52
|
+
* @property {Function} sortBy - Optional sorting function for items (a, b) => number
|
|
53
|
+
*/
|
|
54
|
+
export interface FlowUploadListProps {
|
|
55
|
+
/**
|
|
56
|
+
* Array of flow upload items to display
|
|
57
|
+
*/
|
|
58
|
+
uploads: FlowUploadItem<BrowserUploadInput>[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Optional filter for which items to display
|
|
62
|
+
*/
|
|
63
|
+
filter?: (item: FlowUploadItem<BrowserUploadInput>) => boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Optional sorting function for items
|
|
67
|
+
*/
|
|
68
|
+
sortBy?: (
|
|
69
|
+
a: FlowUploadItem<BrowserUploadInput>,
|
|
70
|
+
b: FlowUploadItem<BrowserUploadInput>,
|
|
71
|
+
) => number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const props = defineProps<FlowUploadListProps>();
|
|
75
|
+
|
|
76
|
+
defineSlots<{
|
|
77
|
+
item(props: {
|
|
78
|
+
item: FlowUploadItem<BrowserUploadInput>;
|
|
79
|
+
index: number;
|
|
80
|
+
isPending: boolean;
|
|
81
|
+
isUploading: boolean;
|
|
82
|
+
isSuccess: boolean;
|
|
83
|
+
isError: boolean;
|
|
84
|
+
isAborted: boolean;
|
|
85
|
+
formatFileSize: (bytes: number) => string;
|
|
86
|
+
}): any;
|
|
87
|
+
default?(props: {
|
|
88
|
+
items: FlowUploadItem<BrowserUploadInput>[];
|
|
89
|
+
itemsByStatus: {
|
|
90
|
+
pending: FlowUploadItem<BrowserUploadInput>[];
|
|
91
|
+
uploading: FlowUploadItem<BrowserUploadInput>[];
|
|
92
|
+
success: FlowUploadItem<BrowserUploadInput>[];
|
|
93
|
+
error: FlowUploadItem<BrowserUploadInput>[];
|
|
94
|
+
aborted: FlowUploadItem<BrowserUploadInput>[];
|
|
95
|
+
};
|
|
96
|
+
}): any;
|
|
97
|
+
}>();
|
|
98
|
+
|
|
99
|
+
// Apply filtering and sorting
|
|
100
|
+
const filteredItems = computed(() => {
|
|
101
|
+
let items = props.uploads;
|
|
102
|
+
|
|
103
|
+
if (props.filter) {
|
|
104
|
+
items = items.filter(props.filter);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (props.sortBy) {
|
|
108
|
+
items = [...items].sort(props.sortBy);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return items;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Group items by status
|
|
115
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
116
|
+
const itemsByStatus = computed(() => ({
|
|
117
|
+
pending: filteredItems.value.filter((item) => item.status === "pending"),
|
|
118
|
+
uploading: filteredItems.value.filter((item) => item.status === "uploading"),
|
|
119
|
+
success: filteredItems.value.filter((item) => item.status === "success"),
|
|
120
|
+
error: filteredItems.value.filter((item) => item.status === "error"),
|
|
121
|
+
aborted: filteredItems.value.filter((item) => item.status === "aborted"),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// Helper function to format file sizes
|
|
125
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
126
|
+
const formatFileSize = (bytes: number): string => {
|
|
127
|
+
if (bytes === 0) return "0 Bytes";
|
|
128
|
+
const k = 1024;
|
|
129
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
130
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
131
|
+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Helper function to get status icon
|
|
135
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
136
|
+
const getStatusIcon = (status: string): string => {
|
|
137
|
+
switch (status) {
|
|
138
|
+
case "pending":
|
|
139
|
+
return "⏳";
|
|
140
|
+
case "uploading":
|
|
141
|
+
return "📤";
|
|
142
|
+
case "success":
|
|
143
|
+
return "✅";
|
|
144
|
+
case "error":
|
|
145
|
+
return "❌";
|
|
146
|
+
case "aborted":
|
|
147
|
+
return "⏹️";
|
|
148
|
+
default:
|
|
149
|
+
return "❓";
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Helper function to get status color
|
|
154
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
155
|
+
const getStatusColor = (status: string): string => {
|
|
156
|
+
switch (status) {
|
|
157
|
+
case "pending":
|
|
158
|
+
return "#6c757d";
|
|
159
|
+
case "uploading":
|
|
160
|
+
return "#007bff";
|
|
161
|
+
case "success":
|
|
162
|
+
return "#28a745";
|
|
163
|
+
case "error":
|
|
164
|
+
return "#dc3545";
|
|
165
|
+
case "aborted":
|
|
166
|
+
return "#6c757d";
|
|
167
|
+
default:
|
|
168
|
+
return "#6c757d";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<template>
|
|
174
|
+
<div class="flow-upload-list">
|
|
175
|
+
<slot :items="filteredItems" :items-by-status="itemsByStatus">
|
|
176
|
+
<!-- Default rendering: simple list of flow upload items -->
|
|
177
|
+
<div
|
|
178
|
+
v-for="(item, index) in filteredItems"
|
|
179
|
+
:key="item.id"
|
|
180
|
+
class="flow-upload-list__item"
|
|
181
|
+
:class="`flow-upload-list__item--${item.status}`"
|
|
182
|
+
>
|
|
183
|
+
<slot
|
|
184
|
+
name="item"
|
|
185
|
+
:item="item"
|
|
186
|
+
:index="index"
|
|
187
|
+
:is-pending="item.status === 'pending'"
|
|
188
|
+
:is-uploading="item.status === 'uploading'"
|
|
189
|
+
:is-success="item.status === 'success'"
|
|
190
|
+
:is-error="item.status === 'error'"
|
|
191
|
+
:is-aborted="item.status === 'aborted'"
|
|
192
|
+
:format-file-size="formatFileSize"
|
|
193
|
+
>
|
|
194
|
+
<!-- Default item template -->
|
|
195
|
+
<div class="flow-upload-list__item-header">
|
|
196
|
+
<span class="flow-upload-list__item-icon">
|
|
197
|
+
{{ getStatusIcon(item.status) }}
|
|
198
|
+
</span>
|
|
199
|
+
<span class="flow-upload-list__item-name">
|
|
200
|
+
{{ isBrowserFile(item.file) ? item.file.name : 'File' }}
|
|
201
|
+
</span>
|
|
202
|
+
<span
|
|
203
|
+
class="flow-upload-list__item-status"
|
|
204
|
+
:style="{ color: getStatusColor(item.status) }"
|
|
205
|
+
>
|
|
206
|
+
{{ item.status.toUpperCase() }}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="flow-upload-list__item-details">
|
|
211
|
+
<span class="flow-upload-list__item-size">
|
|
212
|
+
{{ formatFileSize(item.totalBytes) }}
|
|
213
|
+
</span>
|
|
214
|
+
<span v-if="item.jobId" class="flow-upload-list__item-job">
|
|
215
|
+
Job: {{ item.jobId.slice(0, 8) }}...
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div v-if="item.status === 'uploading'" class="flow-upload-list__item-progress">
|
|
220
|
+
<div class="flow-upload-list__progress-bar">
|
|
221
|
+
<div
|
|
222
|
+
class="flow-upload-list__progress-fill"
|
|
223
|
+
:style="{ width: `${item.progress}%` }"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
<span class="flow-upload-list__progress-text">
|
|
227
|
+
{{ item.progress }}% • {{ formatFileSize(item.bytesUploaded) }} / {{ formatFileSize(item.totalBytes) }}
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div v-if="item.status === 'error' && item.error" class="flow-upload-list__item-error">
|
|
232
|
+
{{ item.error.message }}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div v-if="item.status === 'success'" class="flow-upload-list__item-success">
|
|
236
|
+
Upload complete
|
|
237
|
+
</div>
|
|
238
|
+
</slot>
|
|
239
|
+
</div>
|
|
240
|
+
</slot>
|
|
241
|
+
</div>
|
|
242
|
+
</template>
|
|
243
|
+
|
|
244
|
+
<style scoped>
|
|
245
|
+
.flow-upload-list {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 0.5rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.flow-upload-list__item {
|
|
252
|
+
padding: 0.75rem;
|
|
253
|
+
border: 1px solid #e0e0e0;
|
|
254
|
+
border-radius: 0.375rem;
|
|
255
|
+
background-color: #fff;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.flow-upload-list__item-header {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 0.5rem;
|
|
262
|
+
margin-bottom: 0.5rem;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.flow-upload-list__item-icon {
|
|
266
|
+
font-size: 1rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.flow-upload-list__item-name {
|
|
270
|
+
flex: 1;
|
|
271
|
+
font-weight: 500;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
text-overflow: ellipsis;
|
|
274
|
+
white-space: nowrap;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.flow-upload-list__item-status {
|
|
278
|
+
font-size: 0.75rem;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.flow-upload-list__item-details {
|
|
284
|
+
display: flex;
|
|
285
|
+
gap: 1rem;
|
|
286
|
+
font-size: 0.75rem;
|
|
287
|
+
color: #666;
|
|
288
|
+
margin-bottom: 0.5rem;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.flow-upload-list__item-size {
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.flow-upload-list__item-job {
|
|
296
|
+
color: #999;
|
|
297
|
+
font-family: monospace;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.flow-upload-list__item-progress {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: 0.25rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.flow-upload-list__progress-bar {
|
|
307
|
+
width: 100%;
|
|
308
|
+
height: 0.375rem;
|
|
309
|
+
background-color: #e0e0e0;
|
|
310
|
+
border-radius: 0.1875rem;
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.flow-upload-list__progress-fill {
|
|
315
|
+
height: 100%;
|
|
316
|
+
background-color: #007bff;
|
|
317
|
+
transition: width 0.2s ease;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.flow-upload-list__progress-text {
|
|
321
|
+
font-size: 0.75rem;
|
|
322
|
+
color: #666;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.flow-upload-list__item-error {
|
|
326
|
+
margin-top: 0.5rem;
|
|
327
|
+
padding: 0.5rem;
|
|
328
|
+
background-color: #f8d7da;
|
|
329
|
+
color: #721c24;
|
|
330
|
+
font-size: 0.75rem;
|
|
331
|
+
border-radius: 0.25rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.flow-upload-list__item-success {
|
|
335
|
+
margin-top: 0.5rem;
|
|
336
|
+
padding: 0.5rem;
|
|
337
|
+
background-color: #d4edda;
|
|
338
|
+
color: #155724;
|
|
339
|
+
font-size: 0.75rem;
|
|
340
|
+
border-radius: 0.25rem;
|
|
341
|
+
}
|
|
342
|
+
</style>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FlowUploadZone - Upload zone with flow processing pipeline
|
|
4
|
+
*
|
|
5
|
+
* Specialized upload component that uploads files through a flow pipeline for processing.
|
|
6
|
+
* Supports drag-and-drop and file picker with flow configuration. Emits events for flow
|
|
7
|
+
* completion, errors, and upload status changes.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @example
|
|
11
|
+
* // Upload images through image processing flow
|
|
12
|
+
* <FlowUploadZone
|
|
13
|
+
* :flow-config="{ flowId: 'image-processor' }"
|
|
14
|
+
* accept="image/*"
|
|
15
|
+
* @upload-complete="handleFlowResult"
|
|
16
|
+
* @upload-error="handleError"
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With custom slot content
|
|
21
|
+
* <FlowUploadZone
|
|
22
|
+
* :flow-config="{ flowId: 'image-processor' }"
|
|
23
|
+
* @upload-complete="handleResult"
|
|
24
|
+
* >
|
|
25
|
+
* <template #default="{ isDragging, isProcessing, progress }">
|
|
26
|
+
* <div :class="{ processing: isProcessing }">
|
|
27
|
+
* <p v-if="isProcessing">Processing... {{ progress }}%</p>
|
|
28
|
+
* <p v-else-if="isDragging">Drop file here</p>
|
|
29
|
+
* <p v-else>Drag file here for processing</p>
|
|
30
|
+
* </div>
|
|
31
|
+
* </template>
|
|
32
|
+
* </FlowUploadZone>
|
|
33
|
+
*
|
|
34
|
+
* @emits upload-complete - Flow processing completed with results
|
|
35
|
+
* @emits upload-error - Upload or processing failed
|
|
36
|
+
* @emits upload-start - File upload started
|
|
37
|
+
* @emits validation-error - File validation failed
|
|
38
|
+
*/
|
|
39
|
+
import type {
|
|
40
|
+
FlowUploadConfig,
|
|
41
|
+
FlowUploadOptions,
|
|
42
|
+
} from "@uploadista/client-browser";
|
|
43
|
+
import { computed, ref } from "vue";
|
|
44
|
+
import { useDragDrop, useFlowUpload } from "../composables";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Props for the FlowUploadZone component
|
|
48
|
+
* @property {FlowUploadConfig} flowConfig - Flow configuration with flowId
|
|
49
|
+
* @property {FlowUploadOptions} options - Additional flow upload options
|
|
50
|
+
* @property {string} accept - Accepted file types (single MIME type or extension string)
|
|
51
|
+
* @property {boolean} multiple - Allow multiple files (default: false, flow uploads are single-file)
|
|
52
|
+
* @property {boolean} disabled - Disable the upload zone (default: false)
|
|
53
|
+
* @property {number} maxFileSize - Maximum file size in bytes
|
|
54
|
+
*/
|
|
55
|
+
export interface FlowUploadZoneProps {
|
|
56
|
+
/**
|
|
57
|
+
* Flow configuration
|
|
58
|
+
*/
|
|
59
|
+
flowConfig: FlowUploadConfig;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Additional flow upload options
|
|
63
|
+
*/
|
|
64
|
+
options?: Omit<FlowUploadOptions, "flowConfig">;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Accepted file types (single MIME type or extension string)
|
|
68
|
+
*/
|
|
69
|
+
accept?: string;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether to allow multiple files (currently only single file supported for flow uploads)
|
|
73
|
+
*/
|
|
74
|
+
multiple?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the upload zone is disabled
|
|
78
|
+
*/
|
|
79
|
+
disabled?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Maximum file size in bytes
|
|
83
|
+
*/
|
|
84
|
+
maxFileSize?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const props = withDefaults(defineProps<FlowUploadZoneProps>(), {
|
|
88
|
+
multiple: false,
|
|
89
|
+
disabled: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: Flow result can be any type
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
// biome-ignore lint/suspicious/noExplicitAny: Flow result can be any type
|
|
95
|
+
"upload-complete": [result: any];
|
|
96
|
+
"upload-error": [error: Error];
|
|
97
|
+
"upload-start": [file: File];
|
|
98
|
+
"validation-error": [errors: string[]];
|
|
99
|
+
}>();
|
|
100
|
+
|
|
101
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vue slot definition requires any
|
|
102
|
+
defineSlots<{
|
|
103
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vue slot definition requires any
|
|
104
|
+
default(props: {
|
|
105
|
+
isDragging: boolean;
|
|
106
|
+
isOver: boolean;
|
|
107
|
+
isUploading: boolean;
|
|
108
|
+
isProcessing: boolean;
|
|
109
|
+
progress: number;
|
|
110
|
+
status: string;
|
|
111
|
+
errors: string[];
|
|
112
|
+
openFilePicker: () => void;
|
|
113
|
+
}): any;
|
|
114
|
+
}>();
|
|
115
|
+
|
|
116
|
+
// Initialize flow upload
|
|
117
|
+
const flowUpload = useFlowUpload({
|
|
118
|
+
...props.options,
|
|
119
|
+
flowConfig: props.flowConfig,
|
|
120
|
+
onFlowComplete: (outputs) => {
|
|
121
|
+
emit("upload-complete", outputs);
|
|
122
|
+
props.options?.onFlowComplete?.(outputs);
|
|
123
|
+
},
|
|
124
|
+
onError: (error) => {
|
|
125
|
+
emit("upload-error", error);
|
|
126
|
+
props.options?.onError?.(error);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle files received from drag-drop or file picker
|
|
131
|
+
const handleFilesReceived = (files: File[]) => {
|
|
132
|
+
const file = files[0];
|
|
133
|
+
if (file) {
|
|
134
|
+
emit("upload-start", file);
|
|
135
|
+
flowUpload.upload(file);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Handle validation errors
|
|
140
|
+
const handleValidationError = (errors: string[]) => {
|
|
141
|
+
emit("validation-error", errors);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Initialize drag-drop
|
|
145
|
+
const dragDrop = useDragDrop({
|
|
146
|
+
accept: props.accept ? [props.accept] : undefined,
|
|
147
|
+
multiple: props.multiple,
|
|
148
|
+
maxFileSize: props.maxFileSize,
|
|
149
|
+
onFilesReceived: handleFilesReceived,
|
|
150
|
+
onValidationError: handleValidationError,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// File input ref
|
|
154
|
+
const fileInputRef = ref<HTMLInputElement>();
|
|
155
|
+
|
|
156
|
+
// Open file picker
|
|
157
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
158
|
+
const openFilePicker = () => {
|
|
159
|
+
if (!props.disabled) {
|
|
160
|
+
fileInputRef.value?.click();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Computed states
|
|
165
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
166
|
+
const isActive = computed(
|
|
167
|
+
() => dragDrop.state.value.isDragging || dragDrop.state.value.isOver,
|
|
168
|
+
);
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<template>
|
|
172
|
+
<div
|
|
173
|
+
class="flow-upload-zone"
|
|
174
|
+
:class="{
|
|
175
|
+
'flow-upload-zone--active': isActive,
|
|
176
|
+
'flow-upload-zone--disabled': disabled,
|
|
177
|
+
'flow-upload-zone--uploading': flowUpload.isUploading.value
|
|
178
|
+
}"
|
|
179
|
+
@dragenter="!disabled && dragDrop.onDragEnter"
|
|
180
|
+
@dragover="!disabled && dragDrop.onDragOver"
|
|
181
|
+
@dragleave="!disabled && dragDrop.onDragLeave"
|
|
182
|
+
@drop="!disabled && dragDrop.onDrop"
|
|
183
|
+
@click="openFilePicker"
|
|
184
|
+
role="button"
|
|
185
|
+
:tabindex="disabled ? -1 : 0"
|
|
186
|
+
:aria-disabled="disabled"
|
|
187
|
+
aria-label="Upload file with flow processing"
|
|
188
|
+
@keydown.enter="openFilePicker"
|
|
189
|
+
@keydown.space.prevent="openFilePicker"
|
|
190
|
+
>
|
|
191
|
+
<slot
|
|
192
|
+
:is-dragging="dragDrop.state.value.isDragging"
|
|
193
|
+
:is-over="dragDrop.state.value.isOver"
|
|
194
|
+
:is-uploading="flowUpload.isUploading.value"
|
|
195
|
+
:is-processing="flowUpload.isProcessing.value"
|
|
196
|
+
:progress="flowUpload.state.value.progress"
|
|
197
|
+
:status="flowUpload.state.value.status"
|
|
198
|
+
:errors="[...dragDrop.state.value.errors]"
|
|
199
|
+
:open-file-picker="openFilePicker"
|
|
200
|
+
>
|
|
201
|
+
<!-- Default slot content -->
|
|
202
|
+
<div class="flow-upload-zone__content">
|
|
203
|
+
<p v-if="dragDrop.state.value.isDragging">Drop file here...</p>
|
|
204
|
+
<p v-else-if="flowUpload.isUploading.value">
|
|
205
|
+
Uploading... {{ flowUpload.state.value.progress }}%
|
|
206
|
+
</p>
|
|
207
|
+
<p v-else-if="flowUpload.isProcessing.value">
|
|
208
|
+
Processing...
|
|
209
|
+
<span v-if="flowUpload.state.value.currentNodeName">
|
|
210
|
+
({{ flowUpload.state.value.currentNodeName }})
|
|
211
|
+
</span>
|
|
212
|
+
</p>
|
|
213
|
+
<p v-else-if="flowUpload.state.value.status === 'success'">Upload complete!</p>
|
|
214
|
+
<p v-else-if="flowUpload.state.value.status === 'error'" class="flow-upload-zone__error">
|
|
215
|
+
Error: {{ flowUpload.state.value.error?.message }}
|
|
216
|
+
</p>
|
|
217
|
+
<p v-else>Drag a file here or click to select</p>
|
|
218
|
+
|
|
219
|
+
<div v-if="flowUpload.isUploading.value" class="flow-upload-zone__progress">
|
|
220
|
+
<div class="flow-upload-zone__progress-bar">
|
|
221
|
+
<div
|
|
222
|
+
class="flow-upload-zone__progress-fill"
|
|
223
|
+
:style="{ width: `${flowUpload.state.value.progress}%` }"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div v-if="dragDrop.state.value.errors.length > 0" class="flow-upload-zone__errors">
|
|
229
|
+
<p v-for="(error, index) in dragDrop.state.value.errors" :key="index">
|
|
230
|
+
{{ error }}
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</slot>
|
|
235
|
+
|
|
236
|
+
<input
|
|
237
|
+
ref="fileInputRef"
|
|
238
|
+
type="file"
|
|
239
|
+
:multiple="dragDrop.inputProps.value.multiple"
|
|
240
|
+
:accept="dragDrop.inputProps.value.accept"
|
|
241
|
+
:disabled="disabled"
|
|
242
|
+
@change="dragDrop.onInputChange"
|
|
243
|
+
style="display: none"
|
|
244
|
+
aria-hidden="true"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
</template>
|
|
248
|
+
|
|
249
|
+
<style scoped>
|
|
250
|
+
.flow-upload-zone {
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
user-select: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.flow-upload-zone--disabled {
|
|
256
|
+
cursor: not-allowed;
|
|
257
|
+
opacity: 0.6;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.flow-upload-zone--uploading {
|
|
261
|
+
pointer-events: none;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.flow-upload-zone__content {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
gap: 0.5rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.flow-upload-zone__error {
|
|
273
|
+
color: #dc3545;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.flow-upload-zone__progress {
|
|
277
|
+
width: 100%;
|
|
278
|
+
max-width: 300px;
|
|
279
|
+
margin-top: 0.5rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.flow-upload-zone__progress-bar {
|
|
283
|
+
width: 100%;
|
|
284
|
+
height: 0.5rem;
|
|
285
|
+
background-color: #e0e0e0;
|
|
286
|
+
border-radius: 0.25rem;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.flow-upload-zone__progress-fill {
|
|
291
|
+
height: 100%;
|
|
292
|
+
background-color: #007bff;
|
|
293
|
+
transition: width 0.2s ease;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.flow-upload-zone__errors {
|
|
297
|
+
margin-top: 0.5rem;
|
|
298
|
+
color: #dc3545;
|
|
299
|
+
font-size: 0.875rem;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.flow-upload-zone__errors p {
|
|
303
|
+
margin: 0.25rem 0;
|
|
304
|
+
}
|
|
305
|
+
</style>
|