@uploadista/vue 0.0.20-beta.1 → 0.0.20-beta.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.
@@ -1,305 +0,0 @@
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>
@@ -1,230 +0,0 @@
1
- import type { FlowUploadOptions } from "@uploadista/client-browser";
2
- import type {
3
- FlowManager,
4
- FlowUploadState,
5
- FlowUploadStatus,
6
- } from "@uploadista/client-core";
7
- import type { TypedOutput } from "@uploadista/core/flow";
8
- import { computed, onMounted, onUnmounted, readonly, ref } from "vue";
9
- import { useFlowManagerContext } from "./useFlowManagerContext";
10
-
11
- // Re-export types from core for convenience
12
- export type { FlowUploadState, FlowUploadStatus };
13
-
14
- export interface UseFlowUploadOptions {
15
- /**
16
- * Flow configuration
17
- */
18
- flowConfig: FlowUploadOptions["flowConfig"];
19
-
20
- /**
21
- * Called when upload progress updates
22
- */
23
- onProgress?: (
24
- progress: number,
25
- bytesUploaded: number,
26
- totalBytes: number | null,
27
- ) => void;
28
-
29
- /**
30
- * Called when a chunk completes
31
- */
32
- onChunkComplete?: (
33
- chunkSize: number,
34
- bytesAccepted: number,
35
- bytesTotal: number | null,
36
- ) => void;
37
-
38
- /**
39
- * Called when the flow completes successfully (receives full flow outputs)
40
- * This is the recommended callback for multi-output flows
41
- * Format: { [outputNodeId]: result, ... }
42
- */
43
- onFlowComplete?: (outputs: Record<string, unknown>) => void;
44
-
45
- /**
46
- * Called when upload succeeds (receives typed outputs from all output nodes)
47
- * Each output includes nodeId, optional nodeType, data, and timestamp.
48
- *
49
- * @param outputs - Array of typed outputs from all output nodes
50
- */
51
- onSuccess?: (outputs: TypedOutput[]) => void;
52
-
53
- /**
54
- * Called when upload fails
55
- */
56
- onError?: (error: Error) => void;
57
-
58
- /**
59
- * Called when upload is aborted
60
- */
61
- onAbort?: () => void;
62
-
63
- /**
64
- * Custom retry logic
65
- */
66
- onShouldRetry?: (error: Error, retryAttempt: number) => boolean;
67
- }
68
-
69
- const initialState: FlowUploadState = {
70
- status: "idle",
71
- progress: 0,
72
- bytesUploaded: 0,
73
- totalBytes: null,
74
- error: null,
75
- jobId: null,
76
- flowStarted: false,
77
- currentNodeName: null,
78
- currentNodeType: null,
79
- flowOutputs: null,
80
- };
81
-
82
- /**
83
- * Vue composable for uploading files through a flow.
84
- *
85
- * This composable provides a simple interface for uploading files through a flow.
86
- * The flow handles the upload process and can perform post-processing like
87
- * saving to storage, optimizing images, etc.
88
- *
89
- * Must be used within FlowManagerProvider (which must be within UploadistaProvider).
90
- * Flow events are automatically routed by the provider to the appropriate manager.
91
- *
92
- * @example
93
- * ```vue
94
- * <script setup lang="ts">
95
- * import { useFlowUpload } from '@uploadista/vue';
96
- *
97
- * const flowUpload = useFlowUpload({
98
- * flowConfig: {
99
- * flowId: "my-upload-flow",
100
- * storageId: "my-storage",
101
- * },
102
- * onSuccess: (outputs) => {
103
- * console.log("Flow outputs:", outputs);
104
- * for (const output of outputs) {
105
- * console.log(`${output.nodeId}:`, output.data);
106
- * }
107
- * },
108
- * });
109
- *
110
- * const handleFileChange = (event: Event) => {
111
- * const file = (event.target as HTMLInputElement).files?.[0];
112
- * if (file) flowUpload.upload(file);
113
- * };
114
- * </script>
115
- *
116
- * <template>
117
- * <input type="file" @change="handleFileChange" />
118
- * </template>
119
- * ```
120
- */
121
- export function useFlowUpload(options: UseFlowUploadOptions) {
122
- const { getManager, releaseManager } = useFlowManagerContext();
123
- const state = ref<FlowUploadState>(initialState);
124
- let manager: FlowManager<unknown> | null = null;
125
-
126
- // Store latest options in a ref to access in callbacks
127
- const optionsRef = ref(options);
128
-
129
- // Get or create manager from context when component mounts
130
- onMounted(() => {
131
- const flowId = options.flowConfig.flowId;
132
-
133
- // Create stable callback wrappers
134
- const stableCallbacks = {
135
- onStateChange: (newState: FlowUploadState) => {
136
- state.value = newState;
137
- },
138
- onProgress: (
139
- _uploadId: string,
140
- bytesUploaded: number,
141
- totalBytes: number | null,
142
- ) => {
143
- if (optionsRef.value.onProgress) {
144
- const progress = totalBytes
145
- ? Math.round((bytesUploaded / totalBytes) * 100)
146
- : 0;
147
- optionsRef.value.onProgress(progress, bytesUploaded, totalBytes);
148
- }
149
- },
150
- onChunkComplete: (
151
- chunkSize: number,
152
- bytesAccepted: number,
153
- bytesTotal: number | null,
154
- ) => {
155
- optionsRef.value.onChunkComplete?.(
156
- chunkSize,
157
- bytesAccepted,
158
- bytesTotal,
159
- );
160
- },
161
- onFlowComplete: (outputs: TypedOutput[]) => {
162
- optionsRef.value.onFlowComplete?.(
163
- outputs as unknown as Record<string, unknown>,
164
- );
165
- },
166
- onSuccess: (outputs: TypedOutput[]) => {
167
- optionsRef.value.onSuccess?.(outputs);
168
- },
169
- onError: (error: Error) => {
170
- optionsRef.value.onError?.(error);
171
- },
172
- onAbort: () => {
173
- optionsRef.value.onAbort?.();
174
- },
175
- };
176
-
177
- // Get manager from context
178
- manager = getManager(flowId, stableCallbacks, {
179
- flowConfig: options.flowConfig,
180
- onChunkComplete: options.onChunkComplete,
181
- onFlowComplete: options.onFlowComplete as
182
- | ((outputs: TypedOutput[]) => void)
183
- | undefined,
184
- onSuccess: options.onSuccess,
185
- onError: options.onError,
186
- onAbort: options.onAbort,
187
- onShouldRetry: options.onShouldRetry,
188
- });
189
- });
190
-
191
- // Cleanup on unmount
192
- onUnmounted(() => {
193
- if (manager) {
194
- releaseManager(options.flowConfig.flowId);
195
- manager = null;
196
- }
197
- });
198
-
199
- const upload = async (file: File | Blob) => {
200
- await manager?.upload(file);
201
- };
202
-
203
- const abort = () => {
204
- manager?.abort();
205
- };
206
-
207
- const pause = () => {
208
- manager?.pause();
209
- };
210
-
211
- const reset = () => {
212
- manager?.reset();
213
- };
214
-
215
- return {
216
- state: readonly(state),
217
- upload,
218
- abort,
219
- pause,
220
- reset,
221
- // Derive computed values from state (reactive to state changes)
222
- isUploading: computed(
223
- () =>
224
- state.value.status === "uploading" ||
225
- state.value.status === "processing",
226
- ),
227
- isUploadingFile: computed(() => state.value.status === "uploading"),
228
- isProcessing: computed(() => state.value.status === "processing"),
229
- };
230
- }