bug fixes
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<div v-if="value" class="flex flex-justify-center mb-4">
|
||||
<SMLoading v-if="!imgError && !imgLoaded" class="w-48 h-48" small />
|
||||
<svg
|
||||
v-if="imgError"
|
||||
v-if="imgError && imgLoaded"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-48 text-gray">
|
||||
@@ -13,11 +13,11 @@
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<img
|
||||
v-if="!imgError"
|
||||
class="max-w-48 max-h-48 w-full h-full"
|
||||
@load="imgLoaded = true"
|
||||
@error="imgError = true"
|
||||
:src="mediaGetThumbnail(value, 'medium')" />
|
||||
@load="handleImageLoaded"
|
||||
@error="handleImageError"
|
||||
:style="{ display: image == '' ? 'none' : 'block' }"
|
||||
:src="image" />
|
||||
</div>
|
||||
<svg
|
||||
v-else
|
||||
@@ -38,17 +38,24 @@
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="slots.help"><slot name="help"></slot></template>
|
||||
<input
|
||||
id="file"
|
||||
ref="refUploadInput"
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="props.accepts"
|
||||
@change="handleChangeSelectFile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots } from "vue";
|
||||
import { inject, watch, ref, useSlots, onMounted } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { mediaGetThumbnail } from "../helpers/media";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import SMDialogUpload from "./dialogs/SMDialogUpload.vue";
|
||||
// import SMDialogUpload from "./dialogs/SMDialogUpload.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
import SMLoading from "./SMLoading.vue";
|
||||
|
||||
@@ -168,6 +175,8 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const refUploadInput = ref(null);
|
||||
const image = ref("");
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
@@ -232,6 +241,16 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
mediaGetThumbnail(newValue, "medium", (e) => {
|
||||
image.value = e;
|
||||
imgLoaded.value = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
@@ -269,21 +288,58 @@ const handleMediaSelect = async () => {
|
||||
allowUpload: props.allowUpload,
|
||||
accepts: props.accepts,
|
||||
});
|
||||
} else {
|
||||
result = await openDialog(SMDialogUpload, {
|
||||
accepts: props.accepts,
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
emits("update:modelValue", mediaResult);
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
emits("update:modelValue", mediaResult);
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (refUploadInput.value != null) {
|
||||
refUploadInput.value.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeSelectFile = async () => {
|
||||
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||
imgLoaded.value = false;
|
||||
imgError.value = false;
|
||||
|
||||
const fileList = Array.from(refUploadInput.value.files);
|
||||
|
||||
let file = fileList.length > 0 ? fileList[0] : null;
|
||||
|
||||
emits("update:modelValue", file);
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
control.value = file;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.setTimeout(() => {
|
||||
mediaGetThumbnail(value.value, "medium", (e) => {
|
||||
image.value = e;
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
const handleImageLoaded = () => {
|
||||
imgLoaded.value = true;
|
||||
imgError.value = false;
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
if (image.value !== "") {
|
||||
imgLoaded.value = true;
|
||||
imgError.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ApiOptions {
|
||||
signal?: AbortSignal | null;
|
||||
progress?: ApiProgressCallback;
|
||||
callback?: ApiResultCallback;
|
||||
chunk?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
@@ -322,7 +323,7 @@ export const api = {
|
||||
}
|
||||
|
||||
apiOptions.method = "POST";
|
||||
return await this.send(options);
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
put: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
@@ -335,7 +336,7 @@ export const api = {
|
||||
}
|
||||
|
||||
apiOptions.method = "PUT";
|
||||
return await this.send(options);
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
delete: async function (
|
||||
@@ -350,7 +351,104 @@ export const api = {
|
||||
}
|
||||
|
||||
apiOptions.method = "DELETE";
|
||||
return await this.send(options);
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
chunk: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
// setup api options
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
// set method to post by default
|
||||
if (!Object.prototype.hasOwnProperty.call(apiOptions, "method")) {
|
||||
apiOptions.method = "POST";
|
||||
}
|
||||
|
||||
// check for chunk option
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(apiOptions, "chunk") &&
|
||||
Object.prototype.hasOwnProperty.call(apiOptions, "body") &&
|
||||
apiOptions.body instanceof FormData
|
||||
) {
|
||||
if (apiOptions.body.has(apiOptions.chunk)) {
|
||||
const file = apiOptions.body.get(apiOptions.chunk);
|
||||
|
||||
if (file instanceof File) {
|
||||
const chunkSize = 50 * 1024 * 1024;
|
||||
let chunk = 0;
|
||||
let chunkCount = 1;
|
||||
let job_id = -1;
|
||||
|
||||
if (file.size > chunkSize) {
|
||||
chunkCount = Math.ceil(file.size / chunkSize);
|
||||
}
|
||||
|
||||
let result = null;
|
||||
for (chunk = 0; chunk < chunkCount; chunk++) {
|
||||
const offset = chunk * chunkSize;
|
||||
const fileChunk = file.slice(
|
||||
offset,
|
||||
offset + chunkSize,
|
||||
);
|
||||
|
||||
const chunkFormData = new FormData();
|
||||
if (job_id == -1) {
|
||||
for (const [field, value] of apiOptions.body) {
|
||||
chunkFormData.append(field, value);
|
||||
}
|
||||
|
||||
chunkFormData.append("name", file.name);
|
||||
chunkFormData.append("size", file.size.toString());
|
||||
chunkFormData.append("mime_type", file.type);
|
||||
} else {
|
||||
chunkFormData.append("job_id", job_id.toString());
|
||||
}
|
||||
|
||||
chunkFormData.set(apiOptions.chunk, fileChunk);
|
||||
chunkFormData.append("chunk", (chunk + 1).toString());
|
||||
chunkFormData.append(
|
||||
"chunk_count",
|
||||
chunkCount.toString(),
|
||||
);
|
||||
|
||||
const chunkOptions = {
|
||||
method: apiOptions.method,
|
||||
url: apiOptions.url,
|
||||
params: apiOptions.params || {},
|
||||
body: chunkFormData,
|
||||
headers: apiOptions.headers || {},
|
||||
progress: (progressEvent) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
apiOptions,
|
||||
"progress",
|
||||
)
|
||||
) {
|
||||
apiOptions.progress({
|
||||
loaded:
|
||||
chunk * chunkSize +
|
||||
progressEvent.loaded,
|
||||
total: file.size,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
result = await this.send(chunkOptions);
|
||||
job_id = result.data.media_job.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -88,6 +88,19 @@ export interface MediaCollection {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MediaJob {
|
||||
id: string;
|
||||
media_id: string;
|
||||
user_id: string;
|
||||
status: string;
|
||||
status_text: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface MediaJobResponse {
|
||||
media_job: MediaJob;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
@@ -39,48 +39,55 @@ export const mimeMatches = (
|
||||
};
|
||||
|
||||
export const mediaGetThumbnail = (
|
||||
media: Media,
|
||||
media: Media | File,
|
||||
useVariant: string | null = "",
|
||||
forceRefresh: boolean = false,
|
||||
callback = null,
|
||||
): string => {
|
||||
let url: string = "";
|
||||
|
||||
if (!media) {
|
||||
if (media) {
|
||||
if (media instanceof File) {
|
||||
if (callback != null) {
|
||||
if (mimeMatches("image/*", media.type) == true) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
callback(e.target.result.toString());
|
||||
};
|
||||
|
||||
reader.readAsDataURL(media);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
useVariant &&
|
||||
useVariant != "" &&
|
||||
useVariant != null &&
|
||||
media.variants &&
|
||||
media.variants[useVariant]
|
||||
) {
|
||||
url = media.url.replace(media.name, media.variants[useVariant]);
|
||||
}
|
||||
|
||||
if (media.thumbnail && media.thumbnail.length > 0) {
|
||||
url = media.thumbnail;
|
||||
}
|
||||
|
||||
if (media.variants && media.variants["thumb"]) {
|
||||
url = media.url.replace(media.name, media.variants["thumb"]);
|
||||
}
|
||||
}
|
||||
|
||||
if (url === "") {
|
||||
url = "/assets/fileicons/unknown.webp";
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback(url);
|
||||
return "";
|
||||
}
|
||||
|
||||
if (
|
||||
useVariant &&
|
||||
useVariant != "" &&
|
||||
useVariant != null &&
|
||||
media.variants &&
|
||||
media.variants[useVariant]
|
||||
) {
|
||||
url = media.url.replace(media.name, media.variants[useVariant]);
|
||||
}
|
||||
|
||||
if (media.thumbnail && media.thumbnail.length > 0) {
|
||||
url = media.thumbnail;
|
||||
}
|
||||
|
||||
if (media.variants && media.variants["thumb"]) {
|
||||
url = media.url.replace(media.name, media.variants["thumb"]);
|
||||
}
|
||||
|
||||
if (url === "") {
|
||||
return "/assets/fileicons/unknown.webp";
|
||||
}
|
||||
|
||||
if (forceRefresh == true) {
|
||||
// Generate a random string
|
||||
const randomString = Math.random().toString(36).substring(7);
|
||||
|
||||
// Check if the URL already has query parameters
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
|
||||
// Append the random string as a query parameter
|
||||
url = `${url}${separator}_random=${randomString}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="pageHeading"
|
||||
:back-link="{ name: 'dashboard-media-list' }"
|
||||
back-title="Back to Media" />
|
||||
<SMLoading v-if="form.loading()" />
|
||||
<SMLoading v-if="form.loading()">{{ progressText }}</SMLoading>
|
||||
<div v-else class="max-w-4xl mx-auto px-4 mt-8">
|
||||
<SMForm
|
||||
:model-value="form"
|
||||
@@ -17,7 +17,6 @@
|
||||
<SMSelectFile
|
||||
v-if="!editMultiple"
|
||||
control="file"
|
||||
allow-upload
|
||||
upload-only
|
||||
accepts="*"
|
||||
class="mb-4" />
|
||||
@@ -84,7 +83,11 @@ import { api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { And, Required } from "../../helpers/validate";
|
||||
import { Media, MediaResponse } from "../../helpers/api.types";
|
||||
import {
|
||||
Media,
|
||||
MediaJobResponse,
|
||||
MediaResponse,
|
||||
} from "../../helpers/api.types";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
@@ -96,6 +99,7 @@ import SMPageStatus from "../../components/SMPageStatus.vue";
|
||||
import SMSelectFile from "../../components/SMSelectFile.vue";
|
||||
import { userHasPermission } from "../../helpers/utils";
|
||||
import SMImageGallery from "../../components/SMImageGallery.vue";
|
||||
import { toTitleCase } from "../../helpers/string";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -190,8 +194,10 @@ const handleLoad = async () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async (enableFormCallBack) => {
|
||||
let processing = false;
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
form.loading(true);
|
||||
if (editMultiple === false) {
|
||||
let submitData = new FormData();
|
||||
|
||||
@@ -210,8 +216,9 @@ const handleSubmit = async (enableFormCallBack) => {
|
||||
form.controls.description.value as string,
|
||||
);
|
||||
|
||||
let result = null;
|
||||
if (route.params.id) {
|
||||
await api.put({
|
||||
result = await api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: route.params.id,
|
||||
@@ -226,12 +233,13 @@ const handleSubmit = async (enableFormCallBack) => {
|
||||
)}%`),
|
||||
});
|
||||
} else {
|
||||
await api.post({
|
||||
result = await api.chunk({
|
||||
url: "/media",
|
||||
body: submitData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
chunk: "file",
|
||||
progress: (progressEvent) =>
|
||||
(progressText.value = `Uploading File: ${Math.floor(
|
||||
(progressEvent.loaded / progressEvent.total) * 100,
|
||||
@@ -239,13 +247,79 @@ const handleSubmit = async (enableFormCallBack) => {
|
||||
});
|
||||
}
|
||||
|
||||
useToastStore().addToast({
|
||||
title: route.params.id ? "Media Updated" : "Media Created",
|
||||
content: route.params.id
|
||||
? "The media item has been updated."
|
||||
: "The media item been created.",
|
||||
type: "success",
|
||||
});
|
||||
const mediaJobId = result.data.media_job.id;
|
||||
const mediaJobUpdate = async () => {
|
||||
api.get({
|
||||
url: "/media/job/{id}",
|
||||
params: {
|
||||
id: mediaJobId,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as MediaJobResponse;
|
||||
|
||||
// queued
|
||||
// complete
|
||||
// waiting
|
||||
// processing - txt - prog
|
||||
|
||||
// invalid - err
|
||||
// failed - err
|
||||
|
||||
if (data.media_job.status != "complete") {
|
||||
if (data.media_job.status == "queued") {
|
||||
progressText.value = "Queued for processing";
|
||||
} else if (data.media_job.status == "processing") {
|
||||
if (data.media_job.progress != -1) {
|
||||
progressText.value = `${toTitleCase(
|
||||
data.media_job.status_text,
|
||||
)} ${data.media_job.progress}%`;
|
||||
} else {
|
||||
progressText.value = `${toTitleCase(
|
||||
data.media_job.status_text,
|
||||
)}`;
|
||||
}
|
||||
} else if (
|
||||
data.media_job.status == "invalid" ||
|
||||
data.media_job.status == "failed"
|
||||
) {
|
||||
useToastStore().addToast({
|
||||
title: "Error Processing Media",
|
||||
content: toTitleCase(
|
||||
data.media_job.status_text,
|
||||
),
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
progressText.value = "";
|
||||
form.loading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(mediaJobUpdate, 500);
|
||||
} else {
|
||||
useToastStore().addToast({
|
||||
title: route.params.id
|
||||
? "Media Updated"
|
||||
: "Media Created",
|
||||
content: route.params.id
|
||||
? "The media item has been updated."
|
||||
: "The media item been created.",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
progressText.value = "";
|
||||
form.loading(false);
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("error", e);
|
||||
});
|
||||
};
|
||||
|
||||
processing = true;
|
||||
mediaJobUpdate();
|
||||
} else {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
@@ -292,14 +366,16 @@ const handleSubmit = async (enableFormCallBack) => {
|
||||
}
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const returnUrl = urlParams.get("return");
|
||||
if (returnUrl) {
|
||||
router.push(decodeURIComponent(returnUrl));
|
||||
} else {
|
||||
router.push({ name: "dashboard-media-list" });
|
||||
}
|
||||
// const urlParams = new URLSearchParams(window.location.search);
|
||||
// const returnUrl = urlParams.get("return");
|
||||
// if (returnUrl) {
|
||||
// router.push(decodeURIComponent(returnUrl));
|
||||
// } else {
|
||||
// router.push({ name: "dashboard-media-list" });
|
||||
// }
|
||||
} catch (error) {
|
||||
processing = false;
|
||||
|
||||
useToastStore().addToast({
|
||||
title: "Server error",
|
||||
content: "An error occurred saving the media.",
|
||||
@@ -308,8 +384,10 @@ const handleSubmit = async (enableFormCallBack) => {
|
||||
|
||||
enableFormCallBack();
|
||||
} finally {
|
||||
progressText.value = "";
|
||||
form.loading(false);
|
||||
if (processing == false) {
|
||||
progressText.value = "";
|
||||
form.loading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user