@uploadista/vue 0.0.20-beta.9 → 0.1.0-beta.5
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/dist/components/index.d.mts +3 -3
- package/dist/components/index.mjs +1 -1
- package/dist/{components-CskPs6sR.css → components-B_L33hsM.css} +33 -33
- package/dist/{components-CskPs6sR.css.map → components-B_L33hsM.css.map} +1 -1
- package/dist/components-MZ9ETx9c.mjs +2 -0
- package/dist/components-MZ9ETx9c.mjs.map +1 -0
- package/dist/composables/index.d.mts +1 -1
- package/dist/composables/index.mjs +1 -1
- package/dist/composables-Dny_9Zrg.mjs +2 -0
- package/dist/composables-Dny_9Zrg.mjs.map +1 -0
- package/dist/index-6Scxoy1b.d.mts +1289 -0
- package/dist/index-6Scxoy1b.d.mts.map +1 -0
- package/dist/{index-B2fUTjNP.d.mts → index-BpCRFLJ5.d.mts} +4 -4
- package/dist/index-BpCRFLJ5.d.mts.map +1 -0
- package/dist/{index-DiRR_Ua6.d.mts → index-RY4FPqAk.d.mts} +431 -432
- package/dist/index-RY4FPqAk.d.mts.map +1 -0
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/providers/index.d.mts +1 -1
- package/dist/providers/index.mjs +1 -1
- package/dist/{providers-fqmOwF71.mjs → providers-CjhEBaQV.mjs} +2 -2
- package/dist/providers-CjhEBaQV.mjs.map +1 -0
- package/dist/useUploadistaClient-WVuo8jYH.mjs.map +1 -1
- package/dist/utils/index.d.mts +62 -2
- package/dist/utils/index.d.mts.map +1 -0
- package/package.json +11 -9
- package/src/__tests__/setup.ts +154 -0
- package/src/components/FlowUploadList.vue +25 -24
- package/src/components/UploadList.vue +3 -6
- package/src/components/UploadZone.vue +2 -5
- package/src/components/flow/Flow.vue +16 -4
- package/src/components/flow/FlowDropZone.vue +4 -2
- package/src/components/flow/FlowInput.vue +14 -8
- package/src/components/flow/FlowInputDropZone.vue +4 -2
- package/src/components/flow/FlowInputPreview.vue +3 -1
- package/src/components/flow/FlowProgress.vue +1 -1
- package/src/components/flow/FlowStatus.vue +1 -1
- package/src/components/flow/useFlowContext.ts +7 -5
- package/src/components/index.ts +4 -2
- package/src/components/upload/Upload.vue +146 -0
- package/src/components/upload/UploadCancel.vue +22 -0
- package/src/components/upload/UploadClearCompleted.vue +24 -0
- package/src/components/upload/UploadDropZone.vue +96 -0
- package/src/components/upload/UploadError.vue +42 -0
- package/src/components/upload/UploadItem.vue +54 -0
- package/src/components/upload/UploadItems.vue +33 -0
- package/src/components/upload/UploadProgress.vue +35 -0
- package/src/components/upload/UploadReset.vue +20 -0
- package/src/components/upload/UploadRetry.vue +22 -0
- package/src/components/upload/UploadStartAll.vue +30 -0
- package/src/components/upload/UploadStatus.vue +65 -0
- package/src/components/upload/index.ts +98 -0
- package/src/components/upload/useUploadContext.ts +67 -0
- package/src/composables/eventUtils.test.ts +267 -0
- package/src/composables/eventUtils.ts +5 -4
- package/src/composables/index.ts +1 -1
- package/src/composables/useDragDrop.test.ts +304 -0
- package/src/composables/useFlow.ts +6 -2
- package/src/composables/useFlowManagerContext.ts +5 -1
- package/src/composables/useUploadEvents.ts +1 -4
- package/src/composables/useUploadistaClient.test.ts +152 -0
- package/src/index.ts +65 -4
- package/src/providers/FlowManagerProvider.vue +5 -2
- package/src/utils/index.test.ts +396 -0
- package/src/utils/is-browser-file.test.ts +45 -0
- package/vitest.config.ts +25 -0
- package/dist/components-BxBz_7tS.mjs +0 -2
- package/dist/components-BxBz_7tS.mjs.map +0 -1
- package/dist/composables-BZ2c_WgI.mjs +0 -2
- package/dist/composables-BZ2c_WgI.mjs.map +0 -1
- package/dist/index-B2fUTjNP.d.mts.map +0 -1
- package/dist/index-BLNNvTVx.d.mts +0 -62
- package/dist/index-BLNNvTVx.d.mts.map +0 -1
- package/dist/index-D3PNaPGh.d.mts +0 -787
- package/dist/index-D3PNaPGh.d.mts.map +0 -1
- package/dist/index-DiRR_Ua6.d.mts.map +0 -1
- package/dist/providers-fqmOwF71.mjs.map +0 -1
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { FlowUploadOptions } from "@uploadista/client-browser";
|
|
3
|
+
import type {
|
|
4
|
+
FlowUploadState,
|
|
5
|
+
FlowUploadStatus,
|
|
6
|
+
InputExecutionState,
|
|
7
|
+
} from "@uploadista/client-core";
|
|
3
8
|
import type { TypedOutput } from "@uploadista/core/flow";
|
|
4
|
-
import { provide
|
|
5
|
-
import {
|
|
6
|
-
|
|
9
|
+
import { provide } from "vue";
|
|
10
|
+
import {
|
|
11
|
+
type FlowInputMetadata,
|
|
12
|
+
type UseFlowReturn,
|
|
13
|
+
useFlow,
|
|
14
|
+
} from "../../composables/useFlow";
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* Props for the Flow root component.
|
|
@@ -27,7 +35,11 @@ const emit = defineEmits<{
|
|
|
27
35
|
/** Called when flow fails */
|
|
28
36
|
error: [error: Error];
|
|
29
37
|
/** Called on upload progress */
|
|
30
|
-
progress: [
|
|
38
|
+
progress: [
|
|
39
|
+
uploadId: string,
|
|
40
|
+
bytesUploaded: number,
|
|
41
|
+
totalBytes: number | null,
|
|
42
|
+
];
|
|
31
43
|
/** Called when flow completes with all outputs */
|
|
32
44
|
flowComplete: [outputs: TypedOutput[]];
|
|
33
45
|
/** Called when upload is aborted */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
|
|
4
4
|
import { useFlowContext } from "./useFlowContext";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,7 +28,9 @@ const dragDrop = useDragDrop({
|
|
|
28
28
|
flow.upload(file);
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
-
accept: props.accept
|
|
31
|
+
accept: props.accept
|
|
32
|
+
? props.accept.split(",").map((t) => t.trim())
|
|
33
|
+
: undefined,
|
|
32
34
|
maxFileSize: props.maxFileSize,
|
|
33
35
|
multiple: false,
|
|
34
36
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, provide } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FLOW_INPUT_CONTEXT_KEY,
|
|
5
|
+
type FlowInputContextValue,
|
|
6
|
+
useFlowContext,
|
|
7
|
+
} from "./useFlowContext";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Props for FlowInput component.
|
|
@@ -15,7 +19,7 @@ const flow = useFlowContext();
|
|
|
15
19
|
|
|
16
20
|
// Find metadata for this input
|
|
17
21
|
const metadata = computed(() =>
|
|
18
|
-
flow.inputMetadata.value?.find((m) => m.nodeId === props.nodeId)
|
|
22
|
+
flow.inputMetadata.value?.find((m) => m.nodeId === props.nodeId),
|
|
19
23
|
);
|
|
20
24
|
|
|
21
25
|
// Get current value for this input
|
|
@@ -36,12 +40,14 @@ const contextValue: FlowInputContextValue = {
|
|
|
36
40
|
return props.nodeId;
|
|
37
41
|
},
|
|
38
42
|
get metadata() {
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
return (
|
|
44
|
+
metadata.value ?? {
|
|
45
|
+
nodeId: props.nodeId,
|
|
46
|
+
nodeName: "",
|
|
47
|
+
nodeDescription: "",
|
|
48
|
+
required: false,
|
|
49
|
+
}
|
|
50
|
+
);
|
|
45
51
|
},
|
|
46
52
|
get value() {
|
|
47
53
|
return value.value;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
|
|
4
4
|
import { useFlowInputContext } from "./useFlowContext";
|
|
5
5
|
|
|
6
6
|
// Helper function to check if value is a File (for template use)
|
|
@@ -32,7 +32,9 @@ const dragDrop = useDragDrop({
|
|
|
32
32
|
input.setValue(file);
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
accept: props.accept
|
|
35
|
+
accept: props.accept
|
|
36
|
+
? props.accept.split(",").map((t) => t.trim())
|
|
37
|
+
: undefined,
|
|
36
38
|
maxFileSize: props.maxFileSize,
|
|
37
39
|
multiple: false,
|
|
38
40
|
});
|
|
@@ -5,7 +5,9 @@ import { useFlowInputContext } from "./useFlowContext";
|
|
|
5
5
|
const input = useFlowInputContext();
|
|
6
6
|
|
|
7
7
|
const isFile = computed(() => input.value instanceof File);
|
|
8
|
-
const isUrl = computed(
|
|
8
|
+
const isUrl = computed(
|
|
9
|
+
() => typeof input.value === "string" && (input.value as string).length > 0,
|
|
10
|
+
);
|
|
9
11
|
|
|
10
12
|
const clear = () => {
|
|
11
13
|
input.setValue(undefined);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from "vue";
|
|
3
2
|
import type { FlowUploadStatus } from "@uploadista/client-core";
|
|
4
3
|
import type { TypedOutput } from "@uploadista/core/flow";
|
|
4
|
+
import { computed } from "vue";
|
|
5
5
|
import { useFlowContext } from "./useFlowContext";
|
|
6
6
|
|
|
7
7
|
const flow = useFlowContext();
|
|
@@ -30,11 +30,13 @@ export interface FlowInputContextValue {
|
|
|
30
30
|
/** Set the value for this input */
|
|
31
31
|
setValue: (value: unknown) => void;
|
|
32
32
|
/** Per-input execution state (if available) */
|
|
33
|
-
state:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
state:
|
|
34
|
+
| {
|
|
35
|
+
status: string;
|
|
36
|
+
progress: number;
|
|
37
|
+
error: Error | null;
|
|
38
|
+
}
|
|
39
|
+
| undefined;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
package/src/components/index.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { default as FlowUploadList } from "./FlowUploadList.vue";
|
|
9
|
+
// Flow compound components
|
|
10
|
+
export * from "./flow";
|
|
9
11
|
export { default as UploadList } from "./UploadList.vue";
|
|
10
12
|
export { default as UploadZone } from "./UploadZone.vue";
|
|
11
13
|
|
|
12
|
-
//
|
|
13
|
-
export * from "./
|
|
14
|
+
// Upload compound components
|
|
15
|
+
export * from "./upload";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
3
|
+
import { computed, provide } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
type MultiUploadState,
|
|
6
|
+
type UploadItem,
|
|
7
|
+
useMultiUpload,
|
|
8
|
+
} from "../../composables/useMultiUpload";
|
|
9
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for the Upload root component.
|
|
13
|
+
*/
|
|
14
|
+
export interface UploadProps {
|
|
15
|
+
/** Whether to allow multiple file uploads (default: false) */
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
/** Maximum concurrent uploads (default: 3, only used in multi mode) */
|
|
18
|
+
maxConcurrent?: number;
|
|
19
|
+
/** Whether to auto-start uploads when files are received (default: true) */
|
|
20
|
+
autoStart?: boolean;
|
|
21
|
+
/** Metadata to attach to uploads */
|
|
22
|
+
metadata?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<UploadProps>(), {
|
|
26
|
+
multiple: false,
|
|
27
|
+
maxConcurrent: 3,
|
|
28
|
+
autoStart: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
/** Called when a single file upload succeeds (single mode) */
|
|
33
|
+
success: [result: UploadFile];
|
|
34
|
+
/** Called when an upload fails */
|
|
35
|
+
error: [error: Error, item?: UploadItem];
|
|
36
|
+
/** Called when all uploads complete (multi mode) */
|
|
37
|
+
complete: [
|
|
38
|
+
results: { successful: UploadItem[]; failed: UploadItem[]; total: number },
|
|
39
|
+
];
|
|
40
|
+
/** Called when an individual upload starts */
|
|
41
|
+
uploadStart: [item: UploadItem];
|
|
42
|
+
/** Called on upload progress */
|
|
43
|
+
progress: [
|
|
44
|
+
item: UploadItem,
|
|
45
|
+
progress: number,
|
|
46
|
+
bytesUploaded: number,
|
|
47
|
+
totalBytes: number | null,
|
|
48
|
+
];
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
const multiUpload = useMultiUpload({
|
|
52
|
+
maxConcurrent: props.maxConcurrent,
|
|
53
|
+
metadata: props.metadata,
|
|
54
|
+
onUploadStart: (item) => emit("uploadStart", item),
|
|
55
|
+
onUploadProgress: (item, progress, bytesUploaded, totalBytes) =>
|
|
56
|
+
emit("progress", item, progress, bytesUploaded, totalBytes),
|
|
57
|
+
onUploadSuccess: (_item, result) => {
|
|
58
|
+
// In single mode, call success directly
|
|
59
|
+
if (!props.multiple) {
|
|
60
|
+
emit("success", result);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
onUploadError: (item, error) => {
|
|
64
|
+
emit("error", error, item);
|
|
65
|
+
},
|
|
66
|
+
onComplete: (results) => emit("complete", results),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const handleFilesReceived = (files: File[]) => {
|
|
70
|
+
if (!props.multiple) {
|
|
71
|
+
// Single mode: clear existing and add new file
|
|
72
|
+
multiUpload.clearAll();
|
|
73
|
+
}
|
|
74
|
+
multiUpload.addFiles(files);
|
|
75
|
+
if (props.autoStart) {
|
|
76
|
+
// Use setTimeout to ensure state is updated before starting
|
|
77
|
+
setTimeout(() => multiUpload.startAll(), 0);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Context value provided by the Upload component root.
|
|
83
|
+
* Contains all upload state and actions.
|
|
84
|
+
*/
|
|
85
|
+
export interface UploadContextValue {
|
|
86
|
+
/** Whether in multi-file mode */
|
|
87
|
+
mode: "single" | "multi";
|
|
88
|
+
/** Current multi-upload state (aggregate) */
|
|
89
|
+
state: MultiUploadState;
|
|
90
|
+
/** All upload items */
|
|
91
|
+
items: readonly UploadItem[];
|
|
92
|
+
/** Whether auto-start is enabled */
|
|
93
|
+
autoStart: boolean;
|
|
94
|
+
|
|
95
|
+
/** Add files to the upload queue */
|
|
96
|
+
addFiles: (files: File[]) => void;
|
|
97
|
+
/** Remove an item from the queue */
|
|
98
|
+
removeItem: (id: string) => void;
|
|
99
|
+
/** Start all pending uploads */
|
|
100
|
+
startAll: () => void;
|
|
101
|
+
/** Abort a specific upload by ID */
|
|
102
|
+
abortUpload: (id: string) => void;
|
|
103
|
+
/** Abort all active uploads */
|
|
104
|
+
abortAll: () => void;
|
|
105
|
+
/** Retry a specific failed upload by ID */
|
|
106
|
+
retryUpload: (id: string) => void;
|
|
107
|
+
/** Retry all failed uploads */
|
|
108
|
+
retryFailed: () => void;
|
|
109
|
+
/** Clear all completed uploads */
|
|
110
|
+
clearCompleted: () => void;
|
|
111
|
+
/** Clear all items and reset state */
|
|
112
|
+
clearAll: () => void;
|
|
113
|
+
|
|
114
|
+
/** Internal handler for files received from drop zone */
|
|
115
|
+
handleFilesReceived: (files: File[]) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create computed context value that updates reactively
|
|
119
|
+
// Cast items to mutable array for context (the readonly is enforced at the composable level)
|
|
120
|
+
const contextValue = computed<UploadContextValue>(() => ({
|
|
121
|
+
mode: props.multiple ? "multi" : "single",
|
|
122
|
+
state: multiUpload.state.value,
|
|
123
|
+
items: multiUpload.items.value as UploadItem[],
|
|
124
|
+
autoStart: props.autoStart,
|
|
125
|
+
addFiles: multiUpload.addFiles,
|
|
126
|
+
removeItem: multiUpload.removeItem,
|
|
127
|
+
startAll: multiUpload.startAll,
|
|
128
|
+
abortUpload: multiUpload.abortUpload,
|
|
129
|
+
abortAll: multiUpload.abortAll,
|
|
130
|
+
retryUpload: multiUpload.retryUpload,
|
|
131
|
+
retryFailed: multiUpload.retryFailed,
|
|
132
|
+
clearCompleted: multiUpload.clearCompleted,
|
|
133
|
+
clearAll: multiUpload.clearAll,
|
|
134
|
+
handleFilesReceived,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Provide context for child components
|
|
138
|
+
provide(UPLOAD_CONTEXT_KEY, contextValue);
|
|
139
|
+
|
|
140
|
+
// Expose to parent via defineExpose for programmatic access
|
|
141
|
+
defineExpose(contextValue);
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<template>
|
|
145
|
+
<slot />
|
|
146
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
5
|
+
|
|
6
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
7
|
+
if (!uploadContext) {
|
|
8
|
+
throw new Error("UploadCancel must be used within an <Upload> component.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const isDisabled = computed(() => !uploadContext.value.state.isUploading);
|
|
12
|
+
|
|
13
|
+
const handleClick = () => {
|
|
14
|
+
uploadContext.value.abortAll();
|
|
15
|
+
};
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button type="button" :disabled="isDisabled" @click="handleClick">
|
|
20
|
+
<slot />
|
|
21
|
+
</button>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
5
|
+
|
|
6
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
7
|
+
if (!uploadContext) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"UploadClearCompleted must be used within an <Upload> component.",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const isDisabled = computed(() => uploadContext.value.state.completed === 0);
|
|
14
|
+
|
|
15
|
+
const handleClick = () => {
|
|
16
|
+
uploadContext.value.clearCompleted();
|
|
17
|
+
};
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<button type="button" :disabled="isDisabled" @click="handleClick">
|
|
22
|
+
<slot />
|
|
23
|
+
</button>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, ref } from "vue";
|
|
3
|
+
import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
|
|
4
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
5
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for UploadDropZone component.
|
|
9
|
+
*/
|
|
10
|
+
export interface UploadDropZoneProps {
|
|
11
|
+
/** Accepted file types (e.g., "image/*", ".pdf") */
|
|
12
|
+
accept?: string;
|
|
13
|
+
/** Maximum file size in bytes */
|
|
14
|
+
maxFileSize?: number;
|
|
15
|
+
/** Maximum number of files (only in multi mode) */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Slot props for UploadDropZone component.
|
|
21
|
+
*/
|
|
22
|
+
export interface UploadDropZoneSlotProps {
|
|
23
|
+
/** Whether files are being dragged over */
|
|
24
|
+
isDragging: boolean;
|
|
25
|
+
/** Whether drag is over the zone */
|
|
26
|
+
isOver: boolean;
|
|
27
|
+
/** Validation errors */
|
|
28
|
+
errors: readonly string[];
|
|
29
|
+
/** Drag event handlers to bind to the drop zone element */
|
|
30
|
+
dragHandlers: {
|
|
31
|
+
onDragenter: (event: DragEvent) => void;
|
|
32
|
+
onDragover: (event: DragEvent) => void;
|
|
33
|
+
onDragleave: (event: DragEvent) => void;
|
|
34
|
+
onDrop: (event: DragEvent) => void;
|
|
35
|
+
};
|
|
36
|
+
/** Input props for the hidden file input */
|
|
37
|
+
inputProps: {
|
|
38
|
+
type: "file";
|
|
39
|
+
multiple: boolean;
|
|
40
|
+
accept: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
/** Handler for input change event */
|
|
43
|
+
onInputChange: (event: Event) => void;
|
|
44
|
+
/** Open file picker programmatically */
|
|
45
|
+
openFilePicker: () => void;
|
|
46
|
+
/** Current drag-drop state */
|
|
47
|
+
dragDropState: DragDropState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const props = defineProps<UploadDropZoneProps>();
|
|
51
|
+
|
|
52
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
53
|
+
if (!uploadContext) {
|
|
54
|
+
throw new Error("UploadDropZone must be used within an <Upload> component.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const inputRef = ref<HTMLInputElement>();
|
|
58
|
+
|
|
59
|
+
const dragDrop = useDragDrop({
|
|
60
|
+
onFilesReceived: (files) => uploadContext.value.handleFilesReceived(files),
|
|
61
|
+
accept: props.accept
|
|
62
|
+
? props.accept.split(",").map((t) => t.trim())
|
|
63
|
+
: undefined,
|
|
64
|
+
maxFileSize: props.maxFileSize,
|
|
65
|
+
maxFiles: uploadContext.value.mode === "multi" ? props.maxFiles : 1,
|
|
66
|
+
multiple: uploadContext.value.mode === "multi",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const openFilePicker = () => {
|
|
70
|
+
inputRef.value?.click();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const slotProps = computed<UploadDropZoneSlotProps>(() => ({
|
|
74
|
+
isDragging: dragDrop.state.value.isDragging,
|
|
75
|
+
isOver: dragDrop.state.value.isOver,
|
|
76
|
+
errors: dragDrop.state.value.errors,
|
|
77
|
+
dragHandlers: {
|
|
78
|
+
onDragenter: dragDrop.onDragEnter,
|
|
79
|
+
onDragover: dragDrop.onDragOver,
|
|
80
|
+
onDragleave: dragDrop.onDragLeave,
|
|
81
|
+
onDrop: dragDrop.onDrop,
|
|
82
|
+
},
|
|
83
|
+
inputProps: dragDrop.inputProps.value,
|
|
84
|
+
onInputChange: dragDrop.onInputChange,
|
|
85
|
+
openFilePicker,
|
|
86
|
+
dragDropState: dragDrop.state.value,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
defineExpose({ inputRef });
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<slot v-bind="slotProps">
|
|
94
|
+
<!-- Default slot content if none provided -->
|
|
95
|
+
</slot>
|
|
96
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadItem } from "../../composables/useMultiUpload";
|
|
4
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
5
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Slot props for UploadError component.
|
|
9
|
+
*/
|
|
10
|
+
export interface UploadErrorSlotProps {
|
|
11
|
+
/** Whether there are any errors */
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
/** Number of failed uploads */
|
|
14
|
+
failedCount: number;
|
|
15
|
+
/** Failed items */
|
|
16
|
+
failedItems: readonly UploadItem[];
|
|
17
|
+
/** Reset/clear all errors */
|
|
18
|
+
reset: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
22
|
+
if (!uploadContext) {
|
|
23
|
+
throw new Error("UploadError must be used within an <Upload> component.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const slotProps = computed<UploadErrorSlotProps>(() => {
|
|
27
|
+
const failedItems = uploadContext.value.items.filter((item) =>
|
|
28
|
+
["error", "aborted"].includes(item.state.status),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
hasError: failedItems.length > 0,
|
|
33
|
+
failedCount: failedItems.length,
|
|
34
|
+
failedItems,
|
|
35
|
+
reset: uploadContext.value.clearCompleted,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<slot v-bind="slotProps" />
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, provide } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import {
|
|
5
|
+
UPLOAD_CONTEXT_KEY,
|
|
6
|
+
UPLOAD_ITEM_CONTEXT_KEY,
|
|
7
|
+
type UploadItemContextValue,
|
|
8
|
+
} from "./useUploadContext";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Props for UploadItem component.
|
|
12
|
+
*/
|
|
13
|
+
export interface UploadItemProps {
|
|
14
|
+
/** Item ID */
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Slot props for UploadItem component.
|
|
20
|
+
*/
|
|
21
|
+
export interface UploadItemSlotProps extends UploadItemContextValue {}
|
|
22
|
+
|
|
23
|
+
const props = defineProps<UploadItemProps>();
|
|
24
|
+
|
|
25
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
26
|
+
if (!uploadContext) {
|
|
27
|
+
throw new Error("UploadItem must be used within an <Upload> component.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const item = computed(() =>
|
|
31
|
+
uploadContext.value.items.find((i) => i.id === props.id),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const itemContext = computed<UploadItemContextValue | null>(() => {
|
|
35
|
+
const currentItem = item.value;
|
|
36
|
+
if (!currentItem) return null;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id: props.id,
|
|
40
|
+
file: currentItem.file,
|
|
41
|
+
state: currentItem.state,
|
|
42
|
+
abort: () => uploadContext.value.abortUpload(props.id),
|
|
43
|
+
retry: () => uploadContext.value.retryUpload(props.id),
|
|
44
|
+
remove: () => uploadContext.value.removeItem(props.id),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Provide item context for nested components
|
|
49
|
+
provide(UPLOAD_ITEM_CONTEXT_KEY, itemContext);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<slot v-if="itemContext" v-bind="itemContext" />
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadItem } from "../../composables/useMultiUpload";
|
|
4
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
5
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Slot props for UploadItems component.
|
|
9
|
+
*/
|
|
10
|
+
export interface UploadItemsSlotProps {
|
|
11
|
+
/** All upload items */
|
|
12
|
+
items: readonly UploadItem[];
|
|
13
|
+
/** Whether there are any items */
|
|
14
|
+
hasItems: boolean;
|
|
15
|
+
/** Whether items array is empty */
|
|
16
|
+
isEmpty: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
20
|
+
if (!uploadContext) {
|
|
21
|
+
throw new Error("UploadItems must be used within an <Upload> component.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const slotProps = computed<UploadItemsSlotProps>(() => ({
|
|
25
|
+
items: uploadContext.value.items,
|
|
26
|
+
hasItems: uploadContext.value.items.length > 0,
|
|
27
|
+
isEmpty: uploadContext.value.items.length === 0,
|
|
28
|
+
}));
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<slot v-bind="slotProps" />
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Slot props for UploadProgress component.
|
|
8
|
+
*/
|
|
9
|
+
export interface UploadProgressSlotProps {
|
|
10
|
+
/** Progress percentage (0-100) */
|
|
11
|
+
progress: number;
|
|
12
|
+
/** Bytes uploaded so far */
|
|
13
|
+
bytesUploaded: number;
|
|
14
|
+
/** Total bytes to upload */
|
|
15
|
+
totalBytes: number;
|
|
16
|
+
/** Whether any uploads are active */
|
|
17
|
+
isUploading: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
21
|
+
if (!uploadContext) {
|
|
22
|
+
throw new Error("UploadProgress must be used within an <Upload> component.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const slotProps = computed<UploadProgressSlotProps>(() => ({
|
|
26
|
+
progress: uploadContext.value.state.progress,
|
|
27
|
+
bytesUploaded: uploadContext.value.state.totalBytesUploaded,
|
|
28
|
+
totalBytes: uploadContext.value.state.totalBytes,
|
|
29
|
+
isUploading: uploadContext.value.state.isUploading,
|
|
30
|
+
}));
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<slot v-bind="slotProps" />
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
5
|
+
|
|
6
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
7
|
+
if (!uploadContext) {
|
|
8
|
+
throw new Error("UploadReset must be used within an <Upload> component.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const handleClick = () => {
|
|
12
|
+
uploadContext.value.clearAll();
|
|
13
|
+
};
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<button type="button" @click="handleClick">
|
|
18
|
+
<slot />
|
|
19
|
+
</button>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import type { UploadContextValue } from "./Upload.vue";
|
|
4
|
+
import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
|
|
5
|
+
|
|
6
|
+
const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
|
|
7
|
+
if (!uploadContext) {
|
|
8
|
+
throw new Error("UploadRetry must be used within an <Upload> component.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const isDisabled = computed(() => uploadContext.value.state.failed === 0);
|
|
12
|
+
|
|
13
|
+
const handleClick = () => {
|
|
14
|
+
uploadContext.value.retryFailed();
|
|
15
|
+
};
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button type="button" :disabled="isDisabled" @click="handleClick">
|
|
20
|
+
<slot />
|
|
21
|
+
</button>
|
|
22
|
+
</template>
|