@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.
@@ -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>