@knime/hub-features 1.0.15 → 1.0.17
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/CHANGELOG.md +18 -0
- package/package.json +4 -4
- package/src/common/ofetchClient.ts +12 -0
- package/src/index.ts +1 -0
- package/src/rfcErrors/RFCErrorToastTemplate.vue +157 -0
- package/src/rfcErrors/index.ts +60 -0
- package/src/rfcErrors/types.ts +20 -0
- package/src/useFileUpload/useFileUpload.ts +55 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @knime/hub-features
|
|
2
2
|
|
|
3
|
+
## 1.0.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9fa8163: bugfix/improvements in uploads: retry only on server error, reset placeholder skeleton on prepare upload error
|
|
8
|
+
- Updated dependencies [9fa8163]
|
|
9
|
+
- @knime/components@1.11.1
|
|
10
|
+
- @knime/utils@1.1.3
|
|
11
|
+
|
|
12
|
+
## 1.0.16
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 5b0167e: - Add RFC error utils to handle API error responses
|
|
17
|
+
- Improve error handling for manifest upload request
|
|
18
|
+
- Updated dependencies [5b0167e]
|
|
19
|
+
- @knime/components@1.11.0
|
|
20
|
+
|
|
3
21
|
## 1.0.15
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/hub-features",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Vue components & composables for shared hub features",
|
|
5
5
|
"homepage": "https://knime.github.io/webapps-common/",
|
|
6
6
|
"license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
"@vueuse/core": "10.4.1",
|
|
26
26
|
"@vueuse/shared": "^10.10.0",
|
|
27
27
|
"consola": "3.2.3",
|
|
28
|
+
"ofetch": "^1.4.1",
|
|
28
29
|
"typescript": "^5.4.5",
|
|
29
|
-
"@knime/components": "1.
|
|
30
|
+
"@knime/components": "1.11.1",
|
|
30
31
|
"@knime/styles": "1.1.1",
|
|
31
|
-
"@knime/utils": "1.1.
|
|
32
|
+
"@knime/utils": "1.1.3"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"vue": "3.x"
|
|
@@ -39,7 +40,6 @@
|
|
|
39
40
|
"@vue/test-utils": "2.4.4",
|
|
40
41
|
"vite": "^5.4.7",
|
|
41
42
|
"vite-svg-loader": "4.0.0",
|
|
42
|
-
"vitest-fetch-mock": "^0.4.1",
|
|
43
43
|
"vue-tsc": "^2.0.19"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ofetch } from "ofetch";
|
|
2
|
+
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
headers: {
|
|
5
|
+
"Content-Type": "application/json",
|
|
6
|
+
|
|
7
|
+
// see: https://knime-com.atlassian.net/wiki/spaces/SPECS/pages/4126769212/API+Errors#Client-Request-Headers
|
|
8
|
+
Accept: "application/json; application/problem+json",
|
|
9
|
+
},
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const $ofetch = ofetch.create(defaultConfig);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useClipboard } from "@vueuse/core";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@knime/components";
|
|
6
|
+
import CheckIcon from "@knime/styles/img/icons/check.svg";
|
|
7
|
+
import CopyIcon from "@knime/styles/img/icons/copy.svg";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
headline: string; // toast headline, will not be displayed here but needed when copying to clipboard
|
|
11
|
+
title: string;
|
|
12
|
+
details?: string[];
|
|
13
|
+
status: number;
|
|
14
|
+
date: Date;
|
|
15
|
+
requestId: string;
|
|
16
|
+
errorId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps<Props>();
|
|
20
|
+
const emits = defineEmits(["showMore"]);
|
|
21
|
+
|
|
22
|
+
const { copy, copied } = useClipboard({
|
|
23
|
+
copiedDuring: 3000,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const showDetails = ref(false);
|
|
27
|
+
|
|
28
|
+
const dateFormatOptions = {
|
|
29
|
+
year: "numeric",
|
|
30
|
+
month: "short",
|
|
31
|
+
day: "numeric",
|
|
32
|
+
hour: "numeric",
|
|
33
|
+
minute: "numeric",
|
|
34
|
+
second: "numeric",
|
|
35
|
+
hour12: true,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const formattedDate = computed(() => {
|
|
39
|
+
// eslint-disable-next-line no-undefined
|
|
40
|
+
const formatter = new Intl.DateTimeFormat(undefined, dateFormatOptions); // use default locale
|
|
41
|
+
return formatter.format(props.date);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const errorForClipboard = computed(() => {
|
|
45
|
+
let details = "";
|
|
46
|
+
if (props.details?.length) {
|
|
47
|
+
if (props.details.length > 1) {
|
|
48
|
+
const detailLines = props.details
|
|
49
|
+
.map((item) => `\u2022 ${item}`)
|
|
50
|
+
.join("\n");
|
|
51
|
+
details = `\n${detailLines}`;
|
|
52
|
+
} else {
|
|
53
|
+
details = props.details[0];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let errorText = `${props.headline}\n\n`;
|
|
58
|
+
errorText += `${props.title}\n\n`;
|
|
59
|
+
errorText += details ? `Details: ${details}\n\n` : "";
|
|
60
|
+
|
|
61
|
+
errorText += `Status: ${props.status}\n`;
|
|
62
|
+
errorText += `Date: ${formattedDate.value}\n`;
|
|
63
|
+
errorText += `Request Id: ${props.requestId}\n`;
|
|
64
|
+
errorText += props.errorId ? `Error Id: ${props.errorId}\n` : "";
|
|
65
|
+
|
|
66
|
+
return errorText;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const copyToClipboard = () => {
|
|
70
|
+
copy(errorForClipboard.value);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onShowDetailsClicked = () => {
|
|
74
|
+
showDetails.value = true;
|
|
75
|
+
emits("showMore");
|
|
76
|
+
};
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<div class="wrapper">
|
|
81
|
+
<div class="title">
|
|
82
|
+
{{ props.title }}
|
|
83
|
+
</div>
|
|
84
|
+
<button v-if="!showDetails" class="show-more" @click="onShowDetailsClicked">
|
|
85
|
+
Show details
|
|
86
|
+
</button>
|
|
87
|
+
<div v-if="showDetails" class="additional-info">
|
|
88
|
+
<div v-if="props.details?.length" class="details">
|
|
89
|
+
<strong>Details: </strong>
|
|
90
|
+
<template v-if="props.details.length == 1">
|
|
91
|
+
{{ props.details[0] }}
|
|
92
|
+
</template>
|
|
93
|
+
<template v-else>
|
|
94
|
+
<ul class="details-list">
|
|
95
|
+
<li v-for="(item, index) in details" :key="index">{{ item }}</li>
|
|
96
|
+
</ul>
|
|
97
|
+
</template>
|
|
98
|
+
</div>
|
|
99
|
+
<div><strong>Status: </strong>{{ status }}</div>
|
|
100
|
+
<div><strong>Date: </strong>{{ formattedDate }}</div>
|
|
101
|
+
<div><strong>Request id: </strong>{{ requestId }}</div>
|
|
102
|
+
<div v-if="errorId"><strong>Error id: </strong>{{ errorId }}</div>
|
|
103
|
+
<div class="copy-button-wrapper">
|
|
104
|
+
<Button @click="copyToClipboard">
|
|
105
|
+
<template v-if="copied"
|
|
106
|
+
><CheckIcon class="copy-icon" />Error was copied</template
|
|
107
|
+
>
|
|
108
|
+
<template v-else
|
|
109
|
+
><CopyIcon class="copy-icon" />Copy error to clipboard
|
|
110
|
+
</template>
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style lang="postcss" scoped>
|
|
118
|
+
.wrapper {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 4px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.details {
|
|
125
|
+
margin: 12px 0;
|
|
126
|
+
|
|
127
|
+
& .details-list {
|
|
128
|
+
padding-left: 25px;
|
|
129
|
+
margin-top: 6px;
|
|
130
|
+
margin-bottom: 6px;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.show-more {
|
|
135
|
+
all: unset;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
|
|
140
|
+
&:active,
|
|
141
|
+
&:hover {
|
|
142
|
+
text-decoration: underline;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.copy-button-wrapper {
|
|
147
|
+
& .button {
|
|
148
|
+
font-size: 13px;
|
|
149
|
+
padding: 0;
|
|
150
|
+
margin-top: 10px;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
svg.copy-icon {
|
|
155
|
+
width: 12px;
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { h } from "vue";
|
|
2
|
+
import { FetchError } from "ofetch";
|
|
3
|
+
import type { Toast } from "packages/components/src";
|
|
4
|
+
|
|
5
|
+
import RFCErrorToastTemplate from "./RFCErrorToastTemplate.vue";
|
|
6
|
+
import { RFCError, type RFCErrorData } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Map to a toast template component compatible with an RFCError format
|
|
10
|
+
*/
|
|
11
|
+
const toToast = ({
|
|
12
|
+
headline,
|
|
13
|
+
rfcError,
|
|
14
|
+
}: {
|
|
15
|
+
headline: string;
|
|
16
|
+
rfcError: RFCError;
|
|
17
|
+
}): Toast => {
|
|
18
|
+
const { data } = rfcError;
|
|
19
|
+
|
|
20
|
+
const rfcErrorToastContent = h(RFCErrorToastTemplate, { headline, ...data });
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
type: "error",
|
|
24
|
+
headline,
|
|
25
|
+
component: rfcErrorToastContent,
|
|
26
|
+
autoRemove: false,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Try to parse an ofetch's `FetchError` to an `RFCError`. When parsing is not possible
|
|
32
|
+
* it returns the same `FetchError` given unchanged.
|
|
33
|
+
*/
|
|
34
|
+
const tryParse = (error: FetchError): RFCError | FetchError => {
|
|
35
|
+
if (!error.response) {
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const responseDate = error.response.headers.get("date")
|
|
40
|
+
? new Date(error.response.headers.get("date")!)
|
|
41
|
+
: new Date();
|
|
42
|
+
|
|
43
|
+
const rfcErrorData: RFCErrorData = {
|
|
44
|
+
title: error.data.title as string,
|
|
45
|
+
details: error.data.details as string[] | undefined,
|
|
46
|
+
status: error.statusCode as number,
|
|
47
|
+
date: responseDate,
|
|
48
|
+
requestId: error.response.headers.get("x-request-id") ?? "",
|
|
49
|
+
// eslint-disable-next-line no-undefined
|
|
50
|
+
errorId: error.response.headers.get("x-error-id") ?? undefined,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return new RFCError(rfcErrorData);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const rfcErrors = {
|
|
57
|
+
toToast,
|
|
58
|
+
tryParse,
|
|
59
|
+
RFCError,
|
|
60
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type based on an RFC-9457 error standard (https://www.rfc-editor.org/rfc/rfc9457)
|
|
3
|
+
*/
|
|
4
|
+
export type RFCErrorData = {
|
|
5
|
+
title: string;
|
|
6
|
+
details?: string[];
|
|
7
|
+
status: number;
|
|
8
|
+
date: Date;
|
|
9
|
+
requestId: string;
|
|
10
|
+
errorId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class RFCError extends Error {
|
|
14
|
+
data: RFCErrorData;
|
|
15
|
+
|
|
16
|
+
constructor(data: RFCErrorData) {
|
|
17
|
+
super(data.title);
|
|
18
|
+
this.data = data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { computed, ref } from "vue";
|
|
2
|
+
import { FetchError } from "ofetch";
|
|
2
3
|
|
|
3
4
|
import { useUploadManager } from "@knime/components";
|
|
4
5
|
import { getFileMimeType, promise } from "@knime/utils";
|
|
5
6
|
|
|
7
|
+
import { $ofetch } from "../common/ofetchClient";
|
|
8
|
+
import { rfcErrors } from "../rfcErrors";
|
|
9
|
+
|
|
6
10
|
const DEFAULT_MAX_UPLOAD_QUEUE_SIZE = 10;
|
|
7
11
|
|
|
8
12
|
const DEFAULT_API_BASE_URL = "/_/api";
|
|
9
13
|
|
|
14
|
+
const DEFAULT_RETRY_DELAY_MS = 50;
|
|
15
|
+
|
|
10
16
|
type UseFileUploadOptions = {
|
|
11
17
|
/**
|
|
12
18
|
* Max number of concurrent uploads allowed in the upload queue.
|
|
@@ -35,8 +41,8 @@ type UseFileUploadOptions = {
|
|
|
35
41
|
* ```
|
|
36
42
|
* {
|
|
37
43
|
* "items": {
|
|
38
|
-
* "my-file.txt": { itemContentType: "text/plain" },
|
|
39
|
-
* "folder/my-file.zip": { itemContentType: "application/zip" },
|
|
44
|
+
* "my-file.txt": { itemContentType: "text/plain", itemContentSize: 5000 },
|
|
45
|
+
* "folder/my-file.zip": { itemContentType: "application/zip", itemContentSize: 5000 },
|
|
40
46
|
* }
|
|
41
47
|
* }
|
|
42
48
|
* ```
|
|
@@ -93,7 +99,6 @@ type UseFileUploadOptions = {
|
|
|
93
99
|
* </template>
|
|
94
100
|
* ```
|
|
95
101
|
*/
|
|
96
|
-
|
|
97
102
|
export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
98
103
|
const baseUrl = computed(() => options.apiBaseUrl ?? DEFAULT_API_BASE_URL);
|
|
99
104
|
|
|
@@ -110,7 +115,10 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
|
110
115
|
Object.fromEntries(
|
|
111
116
|
Object.entries(fileDictionary).map(([name, file]) => [
|
|
112
117
|
name,
|
|
113
|
-
{
|
|
118
|
+
{
|
|
119
|
+
itemContentType: getFileMimeType(file),
|
|
120
|
+
itemContentSize: file.size,
|
|
121
|
+
},
|
|
114
122
|
]),
|
|
115
123
|
);
|
|
116
124
|
|
|
@@ -118,58 +126,45 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
|
118
126
|
items: Record<string, { uploadId: string }>;
|
|
119
127
|
};
|
|
120
128
|
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return
|
|
129
|
-
const { uploadId } = items[name];
|
|
130
|
-
const file = fileDictionary[name];
|
|
131
|
-
return { uploadId, file };
|
|
132
|
-
});
|
|
129
|
+
return $ofetch<PrepareUploadResponse>(
|
|
130
|
+
`${baseUrl.value}/repository/${parentId}/manifest`,
|
|
131
|
+
{ method: "POST", body: { items } },
|
|
132
|
+
).then(({ items }) => {
|
|
133
|
+
return Object.keys(items).map((name) => {
|
|
134
|
+
const { uploadId } = items[name];
|
|
135
|
+
const file = fileDictionary[name];
|
|
136
|
+
return { uploadId, file };
|
|
133
137
|
});
|
|
138
|
+
});
|
|
134
139
|
};
|
|
135
140
|
|
|
136
|
-
const resolveFilePartUploadURL =
|
|
137
|
-
uploadId: string,
|
|
138
|
-
partNumber: number,
|
|
139
|
-
) => {
|
|
141
|
+
const resolveFilePartUploadURL = (uploadId: string, partNumber: number) => {
|
|
140
142
|
type UploadURLResponse = {
|
|
141
143
|
method: string;
|
|
142
144
|
url: string;
|
|
143
145
|
header: { Host: string };
|
|
144
146
|
};
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
return $ofetch<UploadURLResponse>(
|
|
147
149
|
`${baseUrl.value}/uploads/${uploadId}/parts/?partNumber=${partNumber}`,
|
|
148
150
|
{ method: "POST" },
|
|
149
151
|
);
|
|
150
|
-
|
|
151
|
-
return response.json() as Promise<UploadURLResponse>;
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
-
const completeUpload =
|
|
154
|
+
const completeUpload = (
|
|
155
155
|
uploadId: string,
|
|
156
156
|
partIds: Record<number, string>,
|
|
157
157
|
) => {
|
|
158
|
-
|
|
158
|
+
return $ofetch(`${baseUrl.value}/uploads/${uploadId}`, {
|
|
159
159
|
method: "POST",
|
|
160
|
-
|
|
161
|
-
body: JSON.stringify(partIds),
|
|
160
|
+
body: partIds,
|
|
162
161
|
});
|
|
163
|
-
|
|
164
|
-
return response.json();
|
|
165
162
|
};
|
|
166
163
|
|
|
167
|
-
const cancelUpload =
|
|
168
|
-
|
|
164
|
+
const cancelUpload = (uploadId: string) => {
|
|
165
|
+
return $ofetch(`${baseUrl.value}/uploads/${uploadId}`, {
|
|
169
166
|
method: "DELETE",
|
|
170
167
|
});
|
|
171
|
-
|
|
172
|
-
return response.json();
|
|
173
168
|
};
|
|
174
169
|
|
|
175
170
|
const useUploadManagerResult = useUploadManager({
|
|
@@ -177,7 +172,7 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
|
177
172
|
|
|
178
173
|
onFileUploadComplete: ({ uploadId, filePartIds }) => {
|
|
179
174
|
promise
|
|
180
|
-
.retryPromise(() => completeUpload(uploadId, filePartIds))
|
|
175
|
+
.retryPromise({ fn: () => completeUpload(uploadId, filePartIds) })
|
|
181
176
|
.then(() => {
|
|
182
177
|
options.onFileUploadComplete?.(uploadId, filePartIds);
|
|
183
178
|
})
|
|
@@ -205,35 +200,48 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
|
205
200
|
return files.slice(0, maxUploadQueueSize - totalPendingUploads.value);
|
|
206
201
|
};
|
|
207
202
|
|
|
208
|
-
const
|
|
203
|
+
const prepareQueueSize = ref(0);
|
|
209
204
|
|
|
210
205
|
return {
|
|
211
206
|
...useUploadManagerResult,
|
|
212
207
|
|
|
213
|
-
isPreparingUpload: computed(() =>
|
|
208
|
+
isPreparingUpload: computed(() => prepareQueueSize.value > 0),
|
|
209
|
+
totalFilesBeingPrepared: computed(() => prepareQueueSize.value),
|
|
214
210
|
|
|
215
211
|
start: async (parentId: string, files: File[]) => {
|
|
212
|
+
const enqueableFiles = getEnqueueableFiles(files);
|
|
216
213
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
if (enqueableFiles.length === 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
prepareQueueSize.value += enqueableFiles.length;
|
|
219
|
+
|
|
220
|
+
const uploadPayload = await promise.retryPromise({
|
|
221
|
+
fn: () => prepareUpload(parentId, enqueableFiles),
|
|
222
|
+
excludeError: (error: FetchError) =>
|
|
223
|
+
// eslint-disable-next-line no-magic-numbers
|
|
224
|
+
Boolean(error.statusCode && error.statusCode < 500),
|
|
225
|
+
retryDelayMS: DEFAULT_RETRY_DELAY_MS,
|
|
226
|
+
});
|
|
225
227
|
|
|
226
228
|
useUploadManagerResult.start(parentId, uploadPayload);
|
|
227
229
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
if (error instanceof FetchError) {
|
|
231
|
+
throw rfcErrors.tryParse(error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw error;
|
|
235
|
+
} finally {
|
|
236
|
+
// errors can only be thrown in the prepareUpload call, useUploadManagerResult.start does its own error handling
|
|
237
|
+
prepareQueueSize.value -= enqueableFiles.length;
|
|
230
238
|
}
|
|
231
239
|
},
|
|
232
240
|
|
|
233
241
|
cancel: (uploadId: string) => {
|
|
234
242
|
useUploadManagerResult.cancel(uploadId);
|
|
235
243
|
cancelUpload(uploadId).catch((error) => {
|
|
236
|
-
consola.error("There was a problem
|
|
244
|
+
consola.error("There was a problem cancelling the upload", { error });
|
|
237
245
|
});
|
|
238
246
|
},
|
|
239
247
|
};
|