@jskit-ai/uploads-image-web 0.1.1 → 0.1.2
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/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/uploads-image-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.2",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Reusable client-side image upload runtime with pre-upload image editing.",
|
|
7
7
|
dependsOn: [
|
|
@@ -43,6 +43,10 @@ export default Object.freeze({
|
|
|
43
43
|
subpath: "./client/composables/createImageUploadRuntime",
|
|
44
44
|
summary: "Exports the reusable image upload runtime factory."
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
subpath: "./client/composables/createManagedImageAssetRuntime",
|
|
48
|
+
summary: "Exports the managed image asset runtime factory for upload and optional delete flows."
|
|
49
|
+
},
|
|
46
50
|
{
|
|
47
51
|
subpath: "./client/styles",
|
|
48
52
|
summary: "Exports Uppy CSS side effects for image upload UIs."
|
|
@@ -61,7 +65,7 @@ export default Object.freeze({
|
|
|
61
65
|
mutations: {
|
|
62
66
|
dependencies: {
|
|
63
67
|
runtime: {
|
|
64
|
-
"@jskit-ai/uploads-runtime": "0.1.
|
|
68
|
+
"@jskit-ai/uploads-runtime": "0.1.2",
|
|
65
69
|
"@uppy/compressor": "^3.1.0",
|
|
66
70
|
"@uppy/core": "^5.2.0",
|
|
67
71
|
"@uppy/dashboard": "^5.1.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/uploads-image-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
10
|
"./client/composables/createImageUploadRuntime": "./src/client/composables/createImageUploadRuntime.js",
|
|
11
|
+
"./client/composables/createManagedImageAssetRuntime": "./src/client/composables/createManagedImageAssetRuntime.js",
|
|
11
12
|
"./client/styles": "./src/client/styles/index.js",
|
|
12
13
|
"./shared": "./src/shared/index.js"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
|
-
"@jskit-ai/uploads-runtime": "0.1.
|
|
16
|
+
"@jskit-ai/uploads-runtime": "0.1.2",
|
|
16
17
|
"@uppy/compressor": "^3.1.0",
|
|
17
18
|
"@uppy/core": "^5.2.0",
|
|
18
19
|
"@uppy/dashboard": "^5.1.1",
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { computed, ref, unref } from "vue";
|
|
2
|
+
import { createImageUploadRuntime } from "./createImageUploadRuntime.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES = Object.freeze({
|
|
5
|
+
endpointUnavailable: "Image endpoint is unavailable.",
|
|
6
|
+
uploadSuccess: "Image uploaded.",
|
|
7
|
+
uploadInvalidResponse: "Image uploaded, but the response payload was invalid.",
|
|
8
|
+
uploadError: "Unable to upload image.",
|
|
9
|
+
uploadRestriction: "Selected image does not meet upload restrictions.",
|
|
10
|
+
editorUnavailable: "Image editor is unavailable in this environment.",
|
|
11
|
+
deleteSuccess: "Image removed.",
|
|
12
|
+
deleteError: "Unable to remove image.",
|
|
13
|
+
deleteUnavailable: "Image removal is unavailable.",
|
|
14
|
+
changeError: "Image updated, but the page could not refresh."
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function normalizeText(value) {
|
|
18
|
+
return String(value || "").trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeObject(value) {
|
|
22
|
+
return value && typeof value === "object" ? value : {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeWritableRef(value, fallbackValue = "") {
|
|
26
|
+
if (value && typeof value === "object" && "value" in value) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return ref(normalizeText(fallbackValue));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function closeDashboard(uppy) {
|
|
34
|
+
const dashboard = uppy?.getPlugin?.("Dashboard");
|
|
35
|
+
if (!dashboard || typeof dashboard.closeModal !== "function") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dashboard.closeModal();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readJsonResponse(response) {
|
|
43
|
+
const contentType = normalizeText(response?.headers?.get?.("content-type")).toLowerCase();
|
|
44
|
+
if (!contentType.includes("application/json")) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return await response.json();
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveMessage(value, fallback) {
|
|
56
|
+
const normalizedValue = normalizeText(value);
|
|
57
|
+
return normalizedValue || fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveUploadErrorFallback({ error, response, fallbackMessage }) {
|
|
61
|
+
const body = response?.body && typeof response.body === "object" ? response.body : {};
|
|
62
|
+
return resolveMessage(body?.error || error?.message, fallbackMessage);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveDeleteErrorFallback({ error, body, fallbackMessage }) {
|
|
66
|
+
const normalizedBody = body && typeof body === "object" ? body : {};
|
|
67
|
+
return resolveMessage(normalizedBody?.error || error?.message, fallbackMessage);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeDeleteResult(result) {
|
|
71
|
+
if (result && typeof result === "object" && ("body" in result || "response" in result)) {
|
|
72
|
+
return {
|
|
73
|
+
body: result.body ?? null,
|
|
74
|
+
response: result.response ?? null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
body: result ?? null,
|
|
80
|
+
response: null
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createManagedImageAssetRuntime({
|
|
85
|
+
uploadEndpoint = "",
|
|
86
|
+
deleteEndpoint = "",
|
|
87
|
+
fieldName = "file",
|
|
88
|
+
hasAsset = false,
|
|
89
|
+
assetVersion = "",
|
|
90
|
+
selectedFileName = null,
|
|
91
|
+
resolveAssetUrl = null,
|
|
92
|
+
resolveRequestHeaders = null,
|
|
93
|
+
onUploadSuccess = null,
|
|
94
|
+
onDeleteSuccess = null,
|
|
95
|
+
resolveUploadErrorMessage = null,
|
|
96
|
+
resolveDeleteErrorMessage = null,
|
|
97
|
+
deleteRequest = null,
|
|
98
|
+
reportFeedback = null,
|
|
99
|
+
messages = {},
|
|
100
|
+
uploadOptions = {},
|
|
101
|
+
deleteMethod = "DELETE",
|
|
102
|
+
source = "uploads-image-web.managed-image-asset",
|
|
103
|
+
dependencies = {}
|
|
104
|
+
} = {}) {
|
|
105
|
+
const createImageUploadRuntimeFactory = dependencies.createImageUploadRuntimeFactory || createImageUploadRuntime;
|
|
106
|
+
const fetchImpl = dependencies.fetchImpl || globalThis.fetch?.bind(globalThis);
|
|
107
|
+
const normalizedMessages = {
|
|
108
|
+
...DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES,
|
|
109
|
+
...normalizeObject(messages)
|
|
110
|
+
};
|
|
111
|
+
const selectedFileNameRef = normalizeWritableRef(selectedFileName);
|
|
112
|
+
const isDeleting = ref(false);
|
|
113
|
+
const normalizedFieldName = normalizeText(fieldName || "file") || "file";
|
|
114
|
+
const normalizedDeleteMethod = normalizeText(deleteMethod || "DELETE").toUpperCase() || "DELETE";
|
|
115
|
+
const normalizedUploadOptions = normalizeObject(uploadOptions);
|
|
116
|
+
|
|
117
|
+
const normalizedUploadEndpoint = computed(() => normalizeText(unref(uploadEndpoint)));
|
|
118
|
+
const normalizedDeleteEndpoint = computed(() => {
|
|
119
|
+
const explicitDeleteEndpoint = normalizeText(unref(deleteEndpoint));
|
|
120
|
+
return explicitDeleteEndpoint || normalizedUploadEndpoint.value;
|
|
121
|
+
});
|
|
122
|
+
const normalizedHasAsset = computed(() => Boolean(unref(hasAsset)));
|
|
123
|
+
const normalizedAssetVersion = computed(() => normalizeText(unref(assetVersion)));
|
|
124
|
+
const assetUrl = computed(() => {
|
|
125
|
+
if (!normalizedHasAsset.value) {
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (typeof resolveAssetUrl !== "function") {
|
|
130
|
+
return normalizedUploadEndpoint.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalizeText(
|
|
134
|
+
resolveAssetUrl({
|
|
135
|
+
uploadEndpoint: normalizedUploadEndpoint.value,
|
|
136
|
+
deleteEndpoint: normalizedDeleteEndpoint.value,
|
|
137
|
+
hasAsset: normalizedHasAsset.value,
|
|
138
|
+
assetVersion: normalizedAssetVersion.value,
|
|
139
|
+
fieldName: normalizedFieldName
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let imageUploadRuntime = null;
|
|
145
|
+
let runtimeUploadEndpoint = "";
|
|
146
|
+
|
|
147
|
+
function report({
|
|
148
|
+
message = "",
|
|
149
|
+
severity = "success",
|
|
150
|
+
channel = "snackbar",
|
|
151
|
+
dedupeKey = ""
|
|
152
|
+
} = {}) {
|
|
153
|
+
const normalizedMessage = normalizeText(message);
|
|
154
|
+
if (!normalizedMessage || typeof reportFeedback !== "function") {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
reportFeedback({
|
|
159
|
+
message: normalizedMessage,
|
|
160
|
+
severity,
|
|
161
|
+
channel,
|
|
162
|
+
dedupeKey: normalizeText(dedupeKey) || `${source}:${severity}:${normalizedMessage}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function resolveHeaders(action, endpoint) {
|
|
167
|
+
if (typeof resolveRequestHeaders !== "function") {
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return normalizeObject(
|
|
172
|
+
await resolveRequestHeaders({
|
|
173
|
+
action,
|
|
174
|
+
uploadEndpoint: normalizedUploadEndpoint.value,
|
|
175
|
+
deleteEndpoint: normalizedDeleteEndpoint.value,
|
|
176
|
+
endpoint: normalizeText(endpoint),
|
|
177
|
+
fieldName: normalizedFieldName
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function updateSelectedFileName(fileName = "") {
|
|
183
|
+
selectedFileNameRef.value = normalizeText(fileName);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runChangeHandler(handler, payload) {
|
|
187
|
+
if (typeof handler !== "function") {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await Promise.resolve(handler(payload));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
report({
|
|
195
|
+
message: resolveMessage(error?.message, normalizedMessages.changeError),
|
|
196
|
+
severity: "error",
|
|
197
|
+
channel: "banner",
|
|
198
|
+
dedupeKey: `${source}:change-error`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveCustomUploadErrorMessage(error, response) {
|
|
204
|
+
const fallbackMessage = resolveUploadErrorFallback({
|
|
205
|
+
error,
|
|
206
|
+
response,
|
|
207
|
+
fallbackMessage: normalizedMessages.uploadError
|
|
208
|
+
});
|
|
209
|
+
if (typeof resolveUploadErrorMessage !== "function") {
|
|
210
|
+
return fallbackMessage;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return resolveMessage(
|
|
214
|
+
resolveUploadErrorMessage({
|
|
215
|
+
error,
|
|
216
|
+
response,
|
|
217
|
+
fieldName: normalizedFieldName,
|
|
218
|
+
defaultMessage: fallbackMessage
|
|
219
|
+
}),
|
|
220
|
+
fallbackMessage
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveCustomDeleteErrorMessage(error, response, body) {
|
|
225
|
+
const fallbackMessage = resolveDeleteErrorFallback({
|
|
226
|
+
error,
|
|
227
|
+
body,
|
|
228
|
+
fallbackMessage: normalizedMessages.deleteError
|
|
229
|
+
});
|
|
230
|
+
if (typeof resolveDeleteErrorMessage !== "function") {
|
|
231
|
+
return fallbackMessage;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return resolveMessage(
|
|
235
|
+
resolveDeleteErrorMessage({
|
|
236
|
+
error,
|
|
237
|
+
response,
|
|
238
|
+
body,
|
|
239
|
+
fieldName: normalizedFieldName,
|
|
240
|
+
defaultMessage: fallbackMessage
|
|
241
|
+
}),
|
|
242
|
+
fallbackMessage
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function destroy() {
|
|
247
|
+
if (!imageUploadRuntime) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
imageUploadRuntime.destroy();
|
|
252
|
+
imageUploadRuntime = null;
|
|
253
|
+
runtimeUploadEndpoint = "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function ensureUploadRuntime() {
|
|
257
|
+
const endpoint = normalizedUploadEndpoint.value;
|
|
258
|
+
if (!endpoint) {
|
|
259
|
+
destroy();
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (imageUploadRuntime && runtimeUploadEndpoint === endpoint) {
|
|
264
|
+
return imageUploadRuntime;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
destroy();
|
|
268
|
+
|
|
269
|
+
imageUploadRuntime = createImageUploadRuntimeFactory({
|
|
270
|
+
...normalizedUploadOptions,
|
|
271
|
+
endpoint,
|
|
272
|
+
fieldName: normalizedFieldName,
|
|
273
|
+
resolveRequestHeaders: async () => resolveHeaders("upload", endpoint),
|
|
274
|
+
onSelectedFileNameChanged: updateSelectedFileName,
|
|
275
|
+
onUploadSuccess: async (payload) => {
|
|
276
|
+
closeDashboard(payload?.uppy);
|
|
277
|
+
updateSelectedFileName("");
|
|
278
|
+
report({
|
|
279
|
+
message: normalizedMessages.uploadSuccess,
|
|
280
|
+
severity: "success",
|
|
281
|
+
channel: "snackbar",
|
|
282
|
+
dedupeKey: `${source}:upload-success`
|
|
283
|
+
});
|
|
284
|
+
await runChangeHandler(onUploadSuccess, payload);
|
|
285
|
+
},
|
|
286
|
+
onInvalidResponse: () => {
|
|
287
|
+
report({
|
|
288
|
+
message: normalizedMessages.uploadInvalidResponse,
|
|
289
|
+
severity: "error",
|
|
290
|
+
channel: "banner",
|
|
291
|
+
dedupeKey: `${source}:upload-invalid-response`
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
onUploadError: ({ error, response }) => {
|
|
295
|
+
report({
|
|
296
|
+
message: resolveCustomUploadErrorMessage(error, response),
|
|
297
|
+
severity: "error",
|
|
298
|
+
channel: "banner",
|
|
299
|
+
dedupeKey: `${source}:upload-error`
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
onRestrictionFailed: ({ error }) => {
|
|
303
|
+
report({
|
|
304
|
+
message: resolveMessage(error?.message, normalizedMessages.uploadRestriction),
|
|
305
|
+
severity: "error",
|
|
306
|
+
channel: "banner",
|
|
307
|
+
dedupeKey: `${source}:upload-restriction`
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
onUnavailable: () => {
|
|
311
|
+
report({
|
|
312
|
+
message: normalizedMessages.editorUnavailable,
|
|
313
|
+
severity: "error",
|
|
314
|
+
channel: "banner",
|
|
315
|
+
dedupeKey: `${source}:editor-unavailable`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
runtimeUploadEndpoint = endpoint;
|
|
320
|
+
|
|
321
|
+
return imageUploadRuntime;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function setup() {
|
|
325
|
+
const runtime = ensureUploadRuntime();
|
|
326
|
+
runtime?.setup?.();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function openEditor() {
|
|
330
|
+
const runtime = ensureUploadRuntime();
|
|
331
|
+
if (!runtime) {
|
|
332
|
+
report({
|
|
333
|
+
message: normalizedMessages.endpointUnavailable,
|
|
334
|
+
severity: "error",
|
|
335
|
+
channel: "banner",
|
|
336
|
+
dedupeKey: `${source}:endpoint-unavailable`
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
runtime.openEditor();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function deleteAsset() {
|
|
345
|
+
if (isDeleting.value) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const endpoint = normalizedDeleteEndpoint.value;
|
|
350
|
+
if (!endpoint) {
|
|
351
|
+
report({
|
|
352
|
+
message: normalizedMessages.deleteUnavailable,
|
|
353
|
+
severity: "error",
|
|
354
|
+
channel: "banner",
|
|
355
|
+
dedupeKey: `${source}:delete-unavailable`
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (typeof deleteRequest !== "function" && typeof fetchImpl !== "function") {
|
|
361
|
+
report({
|
|
362
|
+
message: normalizedMessages.deleteUnavailable,
|
|
363
|
+
severity: "error",
|
|
364
|
+
channel: "banner",
|
|
365
|
+
dedupeKey: `${source}:delete-unavailable`
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let response = null;
|
|
371
|
+
let body = null;
|
|
372
|
+
|
|
373
|
+
isDeleting.value = true;
|
|
374
|
+
try {
|
|
375
|
+
const headers = await resolveHeaders("delete", endpoint);
|
|
376
|
+
|
|
377
|
+
if (typeof deleteRequest === "function") {
|
|
378
|
+
const deleteResult = normalizeDeleteResult(
|
|
379
|
+
await deleteRequest({
|
|
380
|
+
endpoint,
|
|
381
|
+
method: normalizedDeleteMethod,
|
|
382
|
+
headers,
|
|
383
|
+
fetch: fetchImpl,
|
|
384
|
+
uploadEndpoint: normalizedUploadEndpoint.value,
|
|
385
|
+
deleteEndpoint: endpoint,
|
|
386
|
+
fieldName: normalizedFieldName
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
response = deleteResult.response;
|
|
390
|
+
body = deleteResult.body;
|
|
391
|
+
if (response && response.ok === false) {
|
|
392
|
+
const requestError = new Error("Image delete request failed.");
|
|
393
|
+
requestError.response = response;
|
|
394
|
+
requestError.body = body;
|
|
395
|
+
throw requestError;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
response = await fetchImpl(endpoint, {
|
|
399
|
+
method: normalizedDeleteMethod,
|
|
400
|
+
credentials: "include",
|
|
401
|
+
headers
|
|
402
|
+
});
|
|
403
|
+
body = await readJsonResponse(response);
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error("Image delete request failed.");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
updateSelectedFileName("");
|
|
411
|
+
report({
|
|
412
|
+
message: normalizedMessages.deleteSuccess,
|
|
413
|
+
severity: "success",
|
|
414
|
+
channel: "snackbar",
|
|
415
|
+
dedupeKey: `${source}:delete-success`
|
|
416
|
+
});
|
|
417
|
+
await runChangeHandler(onDeleteSuccess, {
|
|
418
|
+
data: body,
|
|
419
|
+
response
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
const errorResponse = response || error?.response || null;
|
|
423
|
+
const errorBody = body ?? error?.body ?? null;
|
|
424
|
+
|
|
425
|
+
report({
|
|
426
|
+
message: resolveCustomDeleteErrorMessage(error, errorResponse, errorBody),
|
|
427
|
+
severity: "error",
|
|
428
|
+
channel: "banner",
|
|
429
|
+
dedupeKey: `${source}:delete-error`
|
|
430
|
+
});
|
|
431
|
+
} finally {
|
|
432
|
+
isDeleting.value = false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return Object.freeze({
|
|
437
|
+
assetUrl,
|
|
438
|
+
deleteAsset,
|
|
439
|
+
destroy,
|
|
440
|
+
hasAsset: normalizedHasAsset,
|
|
441
|
+
isDeleting,
|
|
442
|
+
openEditor,
|
|
443
|
+
selectedFileName: selectedFileNameRef,
|
|
444
|
+
setup
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export {
|
|
449
|
+
createManagedImageAssetRuntime,
|
|
450
|
+
DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES
|
|
451
|
+
};
|
package/src/client/index.js
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { createManagedImageAssetRuntime } from "../src/client/composables/createManagedImageAssetRuntime.js";
|
|
5
|
+
|
|
6
|
+
function createFakeUploadRuntimeFactory(createdRuntimes) {
|
|
7
|
+
return (options = {}) => {
|
|
8
|
+
const runtime = {
|
|
9
|
+
options,
|
|
10
|
+
destroyCount: 0,
|
|
11
|
+
openCount: 0,
|
|
12
|
+
setupCount: 0,
|
|
13
|
+
destroy() {
|
|
14
|
+
this.destroyCount += 1;
|
|
15
|
+
},
|
|
16
|
+
openEditor() {
|
|
17
|
+
this.openCount += 1;
|
|
18
|
+
},
|
|
19
|
+
setup() {
|
|
20
|
+
this.setupCount += 1;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
createdRuntimes.push(runtime);
|
|
25
|
+
return runtime;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createJsonResponse(body, { ok = true } = {}) {
|
|
30
|
+
return {
|
|
31
|
+
ok,
|
|
32
|
+
headers: {
|
|
33
|
+
get(name) {
|
|
34
|
+
return String(name || "").toLowerCase() === "content-type" ? "application/json" : "";
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async json() {
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("createManagedImageAssetRuntime reuses the shared upload lifecycle and delete flow", async () => {
|
|
44
|
+
const createdRuntimes = [];
|
|
45
|
+
const feedbackEvents = [];
|
|
46
|
+
const changedEvents = [];
|
|
47
|
+
const fetchCalls = [];
|
|
48
|
+
const selectedFileName = ref("");
|
|
49
|
+
const uploadEndpoint = ref("/api/pets/1/photo");
|
|
50
|
+
const assetVersion = ref("9");
|
|
51
|
+
const hasAsset = ref(true);
|
|
52
|
+
|
|
53
|
+
const runtime = createManagedImageAssetRuntime({
|
|
54
|
+
uploadEndpoint,
|
|
55
|
+
fieldName: "photo",
|
|
56
|
+
hasAsset,
|
|
57
|
+
assetVersion,
|
|
58
|
+
selectedFileName,
|
|
59
|
+
resolveAssetUrl: ({ uploadEndpoint: endpoint, assetVersion: version }) =>
|
|
60
|
+
version ? `${endpoint}?v=${version}` : endpoint,
|
|
61
|
+
resolveRequestHeaders: async ({ action }) => ({
|
|
62
|
+
"csrf-token": `csrf-${action}`
|
|
63
|
+
}),
|
|
64
|
+
onUploadSuccess: async ({ data }) => {
|
|
65
|
+
changedEvents.push(["upload", data]);
|
|
66
|
+
},
|
|
67
|
+
onDeleteSuccess: async ({ data }) => {
|
|
68
|
+
changedEvents.push(["delete", data]);
|
|
69
|
+
},
|
|
70
|
+
reportFeedback: (payload) => {
|
|
71
|
+
feedbackEvents.push(payload);
|
|
72
|
+
},
|
|
73
|
+
messages: {
|
|
74
|
+
uploadSuccess: "Photo uploaded.",
|
|
75
|
+
deleteSuccess: "Photo removed."
|
|
76
|
+
},
|
|
77
|
+
dependencies: {
|
|
78
|
+
createImageUploadRuntimeFactory: createFakeUploadRuntimeFactory(createdRuntimes),
|
|
79
|
+
fetchImpl: async (url, options = {}) => {
|
|
80
|
+
fetchCalls.push([url, options]);
|
|
81
|
+
return createJsonResponse({ ok: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.equal(runtime.assetUrl.value, "/api/pets/1/photo?v=9");
|
|
87
|
+
|
|
88
|
+
runtime.setup();
|
|
89
|
+
runtime.openEditor();
|
|
90
|
+
assert.equal(createdRuntimes.length, 1);
|
|
91
|
+
assert.equal(createdRuntimes[0].setupCount, 1);
|
|
92
|
+
assert.equal(createdRuntimes[0].openCount, 1);
|
|
93
|
+
assert.equal(createdRuntimes[0].options.fieldName, "photo");
|
|
94
|
+
|
|
95
|
+
createdRuntimes[0].options.onSelectedFileNameChanged("pet.png");
|
|
96
|
+
assert.equal(selectedFileName.value, "pet.png");
|
|
97
|
+
|
|
98
|
+
const dashboard = {
|
|
99
|
+
closeCount: 0,
|
|
100
|
+
closeModal() {
|
|
101
|
+
this.closeCount += 1;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
await createdRuntimes[0].options.onUploadSuccess({
|
|
105
|
+
data: { id: "pet-1" },
|
|
106
|
+
uppy: {
|
|
107
|
+
getPlugin(name) {
|
|
108
|
+
return name === "Dashboard" ? dashboard : null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.equal(selectedFileName.value, "");
|
|
114
|
+
assert.equal(dashboard.closeCount, 1);
|
|
115
|
+
assert.deepEqual(changedEvents, [["upload", { id: "pet-1" }]]);
|
|
116
|
+
assert.equal(feedbackEvents.at(-1)?.message, "Photo uploaded.");
|
|
117
|
+
|
|
118
|
+
await runtime.deleteAsset();
|
|
119
|
+
|
|
120
|
+
assert.equal(runtime.isDeleting.value, false);
|
|
121
|
+
assert.deepEqual(fetchCalls, [
|
|
122
|
+
[
|
|
123
|
+
"/api/pets/1/photo",
|
|
124
|
+
{
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
credentials: "include",
|
|
127
|
+
headers: {
|
|
128
|
+
"csrf-token": "csrf-delete"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
]);
|
|
133
|
+
assert.deepEqual(changedEvents, [
|
|
134
|
+
["upload", { id: "pet-1" }],
|
|
135
|
+
["delete", { ok: true }]
|
|
136
|
+
]);
|
|
137
|
+
assert.equal(feedbackEvents.at(-1)?.message, "Photo removed.");
|
|
138
|
+
|
|
139
|
+
uploadEndpoint.value = "/api/pets/2/photo";
|
|
140
|
+
runtime.openEditor();
|
|
141
|
+
|
|
142
|
+
assert.equal(createdRuntimes.length, 2);
|
|
143
|
+
assert.equal(createdRuntimes[0].destroyCount, 1);
|
|
144
|
+
assert.equal(createdRuntimes[1].openCount, 1);
|
|
145
|
+
assert.equal(createdRuntimes[1].options.endpoint, "/api/pets/2/photo");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("createManagedImageAssetRuntime reports configured upload and delete errors", async () => {
|
|
149
|
+
const createdRuntimes = [];
|
|
150
|
+
const feedbackEvents = [];
|
|
151
|
+
|
|
152
|
+
const runtime = createManagedImageAssetRuntime({
|
|
153
|
+
uploadEndpoint: "/api/pets/1/photo",
|
|
154
|
+
fieldName: "photo",
|
|
155
|
+
reportFeedback: (payload) => {
|
|
156
|
+
feedbackEvents.push(payload);
|
|
157
|
+
},
|
|
158
|
+
resolveUploadErrorMessage: ({ response, defaultMessage }) =>
|
|
159
|
+
response?.body?.fieldErrors?.photo || defaultMessage,
|
|
160
|
+
resolveDeleteErrorMessage: ({ body, defaultMessage }) =>
|
|
161
|
+
body?.fieldErrors?.photo || defaultMessage,
|
|
162
|
+
dependencies: {
|
|
163
|
+
createImageUploadRuntimeFactory: createFakeUploadRuntimeFactory(createdRuntimes),
|
|
164
|
+
fetchImpl: async () =>
|
|
165
|
+
createJsonResponse(
|
|
166
|
+
{
|
|
167
|
+
fieldErrors: {
|
|
168
|
+
photo: "Cannot delete this photo."
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{ ok: false }
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
runtime.openEditor();
|
|
177
|
+
createdRuntimes[0].options.onUploadError({
|
|
178
|
+
error: {
|
|
179
|
+
message: "Request failed."
|
|
180
|
+
},
|
|
181
|
+
response: {
|
|
182
|
+
body: {
|
|
183
|
+
fieldErrors: {
|
|
184
|
+
photo: "Selected photo is invalid."
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await runtime.deleteAsset();
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(
|
|
193
|
+
feedbackEvents.map((entry) => entry.message),
|
|
194
|
+
["Selected photo is invalid.", "Cannot delete this photo."]
|
|
195
|
+
);
|
|
196
|
+
});
|
|
@@ -13,7 +13,12 @@ test("uploads-image-web exports are explicit and aligned with usage", () => {
|
|
|
13
13
|
repoRoot: REPO_ROOT,
|
|
14
14
|
packageDir: PACKAGE_DIR,
|
|
15
15
|
packageId: "@jskit-ai/uploads-image-web",
|
|
16
|
-
requiredExports: [
|
|
16
|
+
requiredExports: [
|
|
17
|
+
"./client",
|
|
18
|
+
"./client/composables/createImageUploadRuntime",
|
|
19
|
+
"./client/composables/createManagedImageAssetRuntime",
|
|
20
|
+
"./shared"
|
|
21
|
+
]
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
assert.deepEqual(result.wildcardExports, []);
|