new media job support
This commit is contained in:
@@ -104,12 +104,25 @@ class MediaWorkerJob implements ShouldQueue
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
if (Storage::disk($storage)->exists($data['name']) === true) {
|
||||
if (array_key_exists('replace', $data) === false || isTrue($data['replace']) === false) {
|
||||
$exists = Storage::disk($storage)->exists($data['name']);
|
||||
if ($exists === true) {
|
||||
if (array_key_exists('noreplace', $data) === true && isTrue($data['noreplace']) === true) {
|
||||
$this->throwMediaJobFailure('file already exists on server');
|
||||
}
|
||||
}
|
||||
|
||||
if($exists === true) {
|
||||
$pathInfo = pathinfo($data['name']);
|
||||
$basename = $pathInfo['filename'];
|
||||
$extension = $pathInfo['extension'];
|
||||
$index = 0;
|
||||
|
||||
do {
|
||||
$index++;
|
||||
$data['name'] = $basename . '-' . $index . '.' . $extension;
|
||||
} while (Storage::disk($storage)->exists($data['name']) === true);
|
||||
}
|
||||
|
||||
if ($media === null) {
|
||||
$newMedia = true;
|
||||
$media = new Media([
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
VNodeProps,
|
||||
watch,
|
||||
} from "vue";
|
||||
|
||||
export interface DialogInstance {
|
||||
@@ -52,10 +53,10 @@ export function closeDialog(data?: unknown) {
|
||||
}
|
||||
|
||||
const lastDialog = dialogRefs.pop();
|
||||
if (data === undefined && lastDialog.comp) {
|
||||
if (data === undefined && lastDialog.comp && lastDialog.comp.returnValue) {
|
||||
data = lastDialog.comp.returnValue();
|
||||
}
|
||||
if (lastDialog) {
|
||||
if (lastDialog && data !== undefined) {
|
||||
lastDialog.resolve(data);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +113,9 @@ export function openDialog<C extends Component>(
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
const autofocusElement = document.querySelector("[autofocus]");
|
||||
const autofocusElement = document.querySelector(
|
||||
"[autofocus]",
|
||||
) as HTMLInputElement;
|
||||
if (autofocusElement) {
|
||||
autofocusElement.focus();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,17 @@
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<img
|
||||
class="max-w-48 max-h-48 w-full h-full"
|
||||
:class="[
|
||||
'max-w-48',
|
||||
'max-h-48',
|
||||
'p-2',
|
||||
'w-full',
|
||||
'h-full',
|
||||
{
|
||||
'border-red-6': feedbackInvalid,
|
||||
'border-2': feedbackInvalid,
|
||||
},
|
||||
]"
|
||||
@load="handleImageLoaded"
|
||||
@error="handleImageError"
|
||||
:style="{ display: image == '' ? 'none' : 'block' }"
|
||||
@@ -29,6 +39,11 @@
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<div class="text-center">
|
||||
<p
|
||||
v-if="feedbackInvalid"
|
||||
class="px-2 -mt-2 pb-2 text-xs text-red-6">
|
||||
{{ feedbackInvalid }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@@ -55,7 +70,6 @@ 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 { Media } from "../helpers/api.types";
|
||||
import SMLoading from "./SMLoading.vue";
|
||||
|
||||
|
||||
@@ -143,16 +143,10 @@
|
||||
{ 'mb-6': showMediaName(item) },
|
||||
]"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
item.status === 'OK'
|
||||
? `url('${mediaGetThumbnail(
|
||||
item,
|
||||
)}')`
|
||||
: 'initial',
|
||||
backgroundColor:
|
||||
item.status === 'OK'
|
||||
? 'initial'
|
||||
: 'rgba(220,220,220,1)',
|
||||
backgroundImage: `url('${mediaGetThumbnail(
|
||||
item,
|
||||
)}')`,
|
||||
backgroundColor: 'initial',
|
||||
}">
|
||||
<div
|
||||
v-if="showMediaName(item)"
|
||||
@@ -160,38 +154,11 @@
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<SMLoading
|
||||
v-if="
|
||||
item.status !== 'OK' &&
|
||||
item.status.startsWith(
|
||||
'Error',
|
||||
) === false
|
||||
"
|
||||
v-if="false"
|
||||
small
|
||||
class="bg-white bg-op-90 w-full h-full"
|
||||
>{{
|
||||
item.status.split(":")
|
||||
.length > 1
|
||||
? item.status
|
||||
.split(":")[1]
|
||||
.trim()
|
||||
: item.status
|
||||
}}</SMLoading
|
||||
>NONE</SMLoading
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
item.status.startsWith(
|
||||
'Error',
|
||||
) === true
|
||||
">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||
fill="rgba(220,38,38,1)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -430,16 +397,9 @@
|
||||
backgroundImage: `url('${mediaGetThumbnail(
|
||||
item,
|
||||
)}')`,
|
||||
backgroundColor:
|
||||
item.status === 'OK'
|
||||
? 'initial'
|
||||
: 'rgba(220,220,220,1)',
|
||||
}">
|
||||
<SMLoading
|
||||
v-if="
|
||||
item.status !== 'OK' &&
|
||||
item.status.startsWith('Error') === false
|
||||
"
|
||||
v-if="false"
|
||||
small
|
||||
class="bg-white bg-op-90 w-full h-full" />
|
||||
<div
|
||||
@@ -996,11 +956,7 @@ const updateFiles = async () => {
|
||||
let remaining = false;
|
||||
|
||||
mediaItems.value.forEach((item, index) => {
|
||||
if (
|
||||
isUUID(item.id) &&
|
||||
item.status != "OK" &&
|
||||
item.status.startsWith("Error") == false
|
||||
) {
|
||||
if (isUUID(item.id)) {
|
||||
remaining = true;
|
||||
|
||||
api.get({
|
||||
@@ -1056,9 +1012,9 @@ const updateFiles = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
mediaItems.value = mediaItems.value.filter(
|
||||
(item) => item.status.startsWith("Error") === false,
|
||||
);
|
||||
// mediaItems.value = mediaItems.value.filter(
|
||||
// (item) => item.status.startsWith("Error") === false,
|
||||
// );
|
||||
|
||||
if (remaining) {
|
||||
updateFilesNonce.value = setTimeout(() => {
|
||||
@@ -1106,7 +1062,6 @@ const handleLoad = async () => {
|
||||
let params = {
|
||||
page: page.value,
|
||||
limit: perPage.value,
|
||||
status: "!Error",
|
||||
filter: "",
|
||||
};
|
||||
|
||||
@@ -1202,8 +1157,8 @@ watch(mediaItems, () => {
|
||||
const computedSelectDisabled = computed(() => {
|
||||
if (selectedTab.value == "tab-browser") {
|
||||
return (
|
||||
selected.value.length == 0 ||
|
||||
selected.value.findIndex((item) => item.status !== "OK") > -1
|
||||
selected.value.length == 0 // ||
|
||||
// selected.value.findIndex((item) => item.status !== "OK") > -1
|
||||
);
|
||||
} else if (selectedTab.value == "tab-url") {
|
||||
return (
|
||||
|
||||
45
resources/js/components/dialogs/SMDialogProgress.vue
Normal file
45
resources/js/components/dialogs/SMDialogProgress.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-black bg-op-20 backdrop-blur"></div>
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bottom-0 flex-justify-center flex-items-center flex">
|
||||
<div
|
||||
class="flex flex-col m-4 border-1 bg-white rounded-xl text-gray-5 px-4 md:px-12 py-4 md:py-8 max-w-200 w-full overflow-hidden">
|
||||
<h2 class="mb-2">{{ props.title }}</h2>
|
||||
<div
|
||||
v-for="(row, index) in props.rows"
|
||||
class="flex flex-col text-xs my-4"
|
||||
:key="index">
|
||||
<div class="w-full bg-gray-3 h-3 mb-2 rounded-2">
|
||||
<div
|
||||
class="bg-sky-600 h-3 rounded-2"
|
||||
:style="{
|
||||
width: `${props.progress[index]}%`,
|
||||
}"></div>
|
||||
</div>
|
||||
<p class="m-0"></p>
|
||||
{{ row }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
progress: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,373 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-black bg-op-20 backdrop-blur"></div>
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bottom-0 flex-justify-center flex-items-center flex">
|
||||
<div
|
||||
class="flex flex-col m-4 border-1 bg-white rounded-xl text-gray-5 px-4 md:px-12 py-4 md:py-8 max-w-200 w-full overflow-hidden"
|
||||
v-if="uploadFileCount > 0">
|
||||
<h2 class="mb-2">Upload Media</h2>
|
||||
<div class="flex flex-col text-xs pb-2 my-4">
|
||||
<div class="w-full bg-gray-3 h-3 mb-2 rounded-2">
|
||||
<div
|
||||
class="bg-sky-600 h-3 rounded-2"
|
||||
:style="{
|
||||
width: `${progressUploadPercent}%`,
|
||||
}"></div>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
{{ progressUploadStatus }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col text-xs my-2">
|
||||
<div class="w-full bg-gray-3 h-3 mb-2 rounded-2">
|
||||
<div
|
||||
class="bg-sky-600 h-3 rounded-2"
|
||||
:style="{
|
||||
width: `${
|
||||
(100 / uploadFileCount) * mediaItems.length
|
||||
}%`,
|
||||
}"></div>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
{{ progressFileStatus }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="file"
|
||||
ref="refUploadInput"
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="computedAccepts"
|
||||
@change="handleChangeSelectFile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { api } from "../../helpers/api";
|
||||
import { ApiInfo, Media, MediaResponse } from "../../helpers/api.types";
|
||||
import { convertFileNameToTitle } from "../../helpers/utils";
|
||||
import { isUUID } from "../../helpers/uuid";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
|
||||
const props = defineProps({
|
||||
mime: {
|
||||
type: String,
|
||||
default: "image/*",
|
||||
required: false,
|
||||
},
|
||||
accepts: {
|
||||
type: String,
|
||||
default: "image/*",
|
||||
required: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Reference to the File Upload Input element.
|
||||
*/
|
||||
const refUploadInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
/**
|
||||
* Max upload size
|
||||
*/
|
||||
const max_upload_size = ref("");
|
||||
|
||||
/**
|
||||
* List of current media items.
|
||||
*/
|
||||
const mediaItems: Ref<Media[]> = ref([]);
|
||||
const processingItems: Ref<Media[]> = ref([]);
|
||||
|
||||
const uploadFileCount = ref(0);
|
||||
const uploadFileNum = ref(0);
|
||||
const uploadFileName = ref("");
|
||||
const uploadFileProgress = ref(0);
|
||||
|
||||
const processFileName = ref("");
|
||||
const processFileStatus = ref("");
|
||||
|
||||
const progressUploadPercent = computed(() => {
|
||||
if (uploadFileCount.value <= uploadFileNum.value) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return (
|
||||
(100 / uploadFileCount.value) * uploadFileNum.value +
|
||||
(100 / uploadFileCount.value / 100) * uploadFileProgress.value
|
||||
);
|
||||
});
|
||||
|
||||
const progressUploadStatus = computed(() => {
|
||||
if (uploadFileCount.value <= uploadFileNum.value) {
|
||||
return `Uploaded ${uploadFileName.value}`;
|
||||
}
|
||||
|
||||
return `Uploading ${uploadFileName.value} - ${uploadFileProgress.value}%`;
|
||||
});
|
||||
|
||||
const progressFileStatus = computed(() => {
|
||||
if (processFileName.value.length > 0) {
|
||||
return (
|
||||
processFileName.value +
|
||||
" - " +
|
||||
(processFileStatus.value.split(":").length > 1
|
||||
? processFileStatus.value.split(":")[1].trim()
|
||||
: processFileStatus.value)
|
||||
);
|
||||
}
|
||||
|
||||
return "Waiting for upload to complete";
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the file types accepted.
|
||||
*/
|
||||
const computedAccepts = computed(() => {
|
||||
if (props.accepts.length > 0) {
|
||||
return props.accepts;
|
||||
}
|
||||
|
||||
if (props.mime.endsWith("/")) {
|
||||
return `${props.mime}*`;
|
||||
}
|
||||
|
||||
return props.mime;
|
||||
});
|
||||
|
||||
const handleChangeSelectFile = async () => {
|
||||
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||
const fileList = Array.from(refUploadInput.value.files);
|
||||
uploadFileCount.value = fileList.length;
|
||||
|
||||
for (
|
||||
uploadFileNum.value = 0;
|
||||
uploadFileNum.value < uploadFileCount.value;
|
||||
uploadFileNum.value++
|
||||
) {
|
||||
const file = fileList[uploadFileNum.value];
|
||||
|
||||
const chunkSize = 50 * 1024 * 1024;
|
||||
let chunk = 0;
|
||||
let chunkCount = 1;
|
||||
let resultMedia = null;
|
||||
|
||||
if (file.size > chunkSize) {
|
||||
chunkCount = Math.ceil(file.size / chunkSize);
|
||||
}
|
||||
|
||||
uploadFileName.value = file.name;
|
||||
uploadFileProgress.value = 0;
|
||||
|
||||
while (chunk < chunkCount) {
|
||||
let submitFormData = new FormData();
|
||||
if (chunkCount == 1) {
|
||||
submitFormData.append("file", file);
|
||||
} else {
|
||||
const offset = chunk * chunkSize;
|
||||
const fileChunk = file.slice(offset, offset + chunkSize);
|
||||
if (resultMedia !== null) {
|
||||
submitFormData.append("id", resultMedia.id);
|
||||
}
|
||||
submitFormData.append("file", fileChunk);
|
||||
submitFormData.append("chunk", (chunk + 1).toString());
|
||||
submitFormData.append("chunk_count", chunkCount.toString());
|
||||
}
|
||||
|
||||
submitFormData.append("name", file.name);
|
||||
submitFormData.append(
|
||||
"title",
|
||||
convertFileNameToTitle(file.name),
|
||||
);
|
||||
submitFormData.append("description", "");
|
||||
|
||||
try {
|
||||
let result = await api.post({
|
||||
url: "/media",
|
||||
body: submitFormData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
progress: (progressEvent) => {
|
||||
uploadFileProgress.value = Math.floor(
|
||||
((chunk * chunkSize + progressEvent.loaded) /
|
||||
file.size) *
|
||||
100,
|
||||
);
|
||||
},
|
||||
});
|
||||
if (result.data) {
|
||||
resultMedia = (result.data as MediaResponse).medium;
|
||||
}
|
||||
} catch (error) {
|
||||
let errorString = "A server error occurred";
|
||||
|
||||
if (error.status == 413) {
|
||||
errorString = `The file is larger than ${max_upload_size.value}`;
|
||||
}
|
||||
|
||||
useToastStore().addToast({
|
||||
title: "Upload failed",
|
||||
type: "danger",
|
||||
content: errorString,
|
||||
});
|
||||
|
||||
resultMedia = null;
|
||||
break;
|
||||
}
|
||||
|
||||
chunk++;
|
||||
}
|
||||
|
||||
if (resultMedia != null) {
|
||||
processingItems.value.push(resultMedia);
|
||||
}
|
||||
processFiles();
|
||||
}
|
||||
} else {
|
||||
closeDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processFilesNonce = ref(null);
|
||||
|
||||
const processFiles = async () => {
|
||||
if (processFilesNonce.value == null) {
|
||||
let remaining = false;
|
||||
|
||||
for (let i = 0; i < processingItems.value.length; i++) {
|
||||
let item = processingItems.value[i];
|
||||
let breakLoop = true;
|
||||
|
||||
if (
|
||||
isUUID(item.id) &&
|
||||
item.status != "OK" &&
|
||||
item.status.startsWith("Error") == false
|
||||
) {
|
||||
remaining = true;
|
||||
|
||||
api.get({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((updateResult) => {
|
||||
if (updateResult.data) {
|
||||
let removeItem = false;
|
||||
|
||||
const updateData =
|
||||
updateResult.data as MediaResponse;
|
||||
if (updateData.medium.status == "OK") {
|
||||
mediaItems.value.push(updateData.medium);
|
||||
removeItem = true;
|
||||
} else if (
|
||||
updateData.medium.status.startsWith("Error") ===
|
||||
true
|
||||
) {
|
||||
removeItem = true;
|
||||
|
||||
useToastStore().addToast({
|
||||
title: "Upload failed",
|
||||
type: "danger",
|
||||
content: updateData.medium.status,
|
||||
});
|
||||
} else {
|
||||
processFileName.value = updateData.medium.name;
|
||||
processFileStatus.value =
|
||||
updateData.medium.status;
|
||||
breakLoop = true;
|
||||
}
|
||||
|
||||
if (removeItem) {
|
||||
processingItems.value =
|
||||
processingItems.value.filter(
|
||||
(mediaItem) =>
|
||||
mediaItem.id !==
|
||||
updateData.medium.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw "error";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* error retreiving data */
|
||||
processingItems.value = processingItems.value.filter(
|
||||
(mediaItem) => mediaItem.id !== item.id,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!breakLoop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining) {
|
||||
processFilesNonce.value = setTimeout(() => {
|
||||
processFilesNonce.value = null;
|
||||
processFiles();
|
||||
}, 500);
|
||||
} else {
|
||||
processFilesNonce.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (processFilesNonce.value == null) {
|
||||
if (mediaItems.value.length == 0) {
|
||||
closeDialog(false);
|
||||
}
|
||||
|
||||
if (props.multiple == false) {
|
||||
closeDialog(mediaItems.value[0]);
|
||||
}
|
||||
|
||||
closeDialog(mediaItems.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Get max upload size
|
||||
api.get({
|
||||
url: "",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as ApiInfo;
|
||||
|
||||
max_upload_size.value = bytesReadable(data.max_upload_size);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
const handleFocus = () => {
|
||||
window.setTimeout(() => {
|
||||
if (uploadFileCount.value == 0) {
|
||||
closeDialog(false);
|
||||
}
|
||||
}, 20);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
if (refUploadInput.value != null) {
|
||||
refUploadInput.value.click();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
});
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@ interface ApiCallbackData {
|
||||
type ApiProgressCallback = (progress: ApiProgressData) => void;
|
||||
type ApiResultCallback = (data: ApiCallbackData) => void;
|
||||
|
||||
interface ApiOptions {
|
||||
export interface ApiOptions {
|
||||
url: string;
|
||||
params?: object;
|
||||
method?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="pageHeading"
|
||||
:back-link="{ name: 'dashboard-media-list' }"
|
||||
back-title="Back to Media" />
|
||||
<SMLoading v-if="form.loading()">{{ progressText }}</SMLoading>
|
||||
<SMLoading v-if="form.loading()" />
|
||||
<div v-else class="max-w-4xl mx-auto px-4 mt-8">
|
||||
<SMForm
|
||||
:model-value="form"
|
||||
@@ -77,9 +77,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { api } from "../../helpers/api";
|
||||
import { ApiOptions, api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { And, Required } from "../../helpers/validate";
|
||||
@@ -88,7 +88,7 @@ import {
|
||||
MediaJobResponse,
|
||||
MediaResponse,
|
||||
} from "../../helpers/api.types";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import { closeDialog, openDialog } from "../../components/SMDialog";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
@@ -100,6 +100,7 @@ import SMSelectFile from "../../components/SMSelectFile.vue";
|
||||
import { userHasPermission } from "../../helpers/utils";
|
||||
import SMImageGallery from "../../components/SMImageGallery.vue";
|
||||
import { toTitleCase } from "../../helpers/string";
|
||||
import SMDialogProgress from "../../components/dialogs/SMDialogProgress.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -111,13 +112,12 @@ const pageHeading = route.params.id
|
||||
? "Edit Multiple Media"
|
||||
: "Edit Media"
|
||||
: "Upload Media";
|
||||
const progressText = ref("");
|
||||
const galleryItems = ref([]);
|
||||
|
||||
const form = reactive(
|
||||
Form({
|
||||
file: FormControl("", And([Required()])),
|
||||
title: FormControl(),
|
||||
title: FormControl("", Required()),
|
||||
description: FormControl(),
|
||||
permission: FormControl(),
|
||||
}),
|
||||
@@ -193,204 +193,271 @@ const handleLoad = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (enableFormCallBack) => {
|
||||
let processing = false;
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (editMultiple === false) {
|
||||
let submitData = new FormData();
|
||||
|
||||
// add file if there is one
|
||||
if (form.controls.file.value instanceof File) {
|
||||
submitData.append("file", form.controls.file.value);
|
||||
}
|
||||
|
||||
submitData.append("title", form.controls.title.value as string);
|
||||
submitData.append(
|
||||
"permission",
|
||||
form.controls.permission.value as string,
|
||||
);
|
||||
submitData.append(
|
||||
"description",
|
||||
form.controls.description.value as string,
|
||||
);
|
||||
|
||||
let result = null;
|
||||
if (route.params.id) {
|
||||
result = await api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
body: submitData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
progress: (progressEvent) =>
|
||||
(progressText.value = `Uploading File: ${Math.floor(
|
||||
(progressEvent.loaded / progressEvent.total) * 100,
|
||||
)}%`),
|
||||
});
|
||||
} else {
|
||||
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,
|
||||
)}%`),
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
(route.params.id as string).split(",").forEach(async (id) => {
|
||||
try {
|
||||
let data = {
|
||||
title: form.controls.title.value,
|
||||
content: form.controls.content.value,
|
||||
};
|
||||
|
||||
await api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: id,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (errorCount === 0) {
|
||||
useToastStore().addToast({
|
||||
title: "Media Updated",
|
||||
content: `The selected media have been updated.`,
|
||||
type: "success",
|
||||
});
|
||||
} else if (successCount === 0) {
|
||||
useToastStore().addToast({
|
||||
title: "Error Updating Media",
|
||||
content: "An unexpected server error occurred.",
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
useToastStore().addToast({
|
||||
title: "Some Media Updated",
|
||||
content: `Only ${successCount} media items where updated. ${errorCount} could not because of an unexpected error.`,
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
const dialogDataSetStatus = (dialogData, status, progress, add) => {
|
||||
if (add) {
|
||||
dialogData.rows.push(status);
|
||||
dialogData.progress.push(progress);
|
||||
} else {
|
||||
const index = dialogData.rows.length - 1;
|
||||
if (status.length > 0) {
|
||||
dialogData.rows[index] = status;
|
||||
}
|
||||
|
||||
// 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.",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
enableFormCallBack();
|
||||
} finally {
|
||||
if (processing == false) {
|
||||
progressText.value = "";
|
||||
form.loading(false);
|
||||
if (progress > -1) {
|
||||
dialogData.progress[index] = progress;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (enableFormCallBack) => {
|
||||
if (editMultiple === false) {
|
||||
let dialogData = ref({
|
||||
title: "Upload Media",
|
||||
rows: [],
|
||||
progress: [],
|
||||
});
|
||||
|
||||
openDialog(SMDialogProgress, dialogData.value);
|
||||
let submitData = new FormData();
|
||||
|
||||
// add file if there is one
|
||||
if (form.controls.file.value instanceof File) {
|
||||
submitData.append("file", form.controls.file.value);
|
||||
dialogDataSetStatus(
|
||||
dialogData.value,
|
||||
`Uploading File: ${form.controls.file.value.name}`,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
submitData.append("title", form.controls.title.value as string);
|
||||
submitData.append(
|
||||
"permission",
|
||||
form.controls.permission.value as string,
|
||||
);
|
||||
submitData.append(
|
||||
"description",
|
||||
form.controls.description.value as string,
|
||||
);
|
||||
|
||||
let apiRequest: ApiOptions = {
|
||||
url: "/media",
|
||||
body: submitData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
progress: (progressEvent) => {
|
||||
dialogDataSetStatus(
|
||||
dialogData.value,
|
||||
"",
|
||||
Math.floor(
|
||||
(progressEvent.loaded / progressEvent.total) * 100,
|
||||
),
|
||||
false,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
if (submitData.has("file") == true) {
|
||||
apiRequest.chunk = "file";
|
||||
}
|
||||
|
||||
if (route.params.id) {
|
||||
apiRequest.url = "/media/{id}";
|
||||
apiRequest.method = "PUT";
|
||||
apiRequest.params = {
|
||||
id: route.params.id,
|
||||
};
|
||||
}
|
||||
|
||||
api.chunk(apiRequest)
|
||||
.then((result) => {
|
||||
if (submitData.has("file") == true) {
|
||||
dialogDataSetStatus(
|
||||
dialogData.value,
|
||||
"Upload Complete",
|
||||
100,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
dialogDataSetStatus(dialogData.value, "Processing", 0, true);
|
||||
|
||||
const mediaJobId = (result.data as MediaJobResponse).media_job
|
||||
.id;
|
||||
const mediaJobUpdate = async () => {
|
||||
api.get({
|
||||
url: "/media/job/{id}",
|
||||
params: {
|
||||
id: mediaJobId,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as MediaJobResponse;
|
||||
|
||||
const statusText = toTitleCase(
|
||||
data.media_job.status_text,
|
||||
);
|
||||
|
||||
if (data.media_job.status != "complete") {
|
||||
if (data.media_job.status == "queued") {
|
||||
dialogDataSetStatus(
|
||||
dialogData.value,
|
||||
"Queued for processing",
|
||||
0,
|
||||
false,
|
||||
);
|
||||
} else if (
|
||||
data.media_job.status == "processing"
|
||||
) {
|
||||
dialogDataSetStatus(
|
||||
dialogData.value,
|
||||
statusText,
|
||||
data.media_job.progress,
|
||||
false,
|
||||
);
|
||||
} else if (
|
||||
data.media_job.status == "invalid" ||
|
||||
data.media_job.status == "failed"
|
||||
) {
|
||||
useToastStore().addToast({
|
||||
title: "Error Processing Media",
|
||||
content: statusText,
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
form.controls.file.setValidationResult(
|
||||
false,
|
||||
statusText,
|
||||
);
|
||||
|
||||
closeDialog();
|
||||
enableFormCallBack();
|
||||
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",
|
||||
});
|
||||
|
||||
closeDialog();
|
||||
enableFormCallBack();
|
||||
|
||||
// return to dashboard
|
||||
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",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
useToastStore().addToast({
|
||||
title: "Error Uploading Media",
|
||||
content: "A server error occurred.",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
closeDialog();
|
||||
enableFormCallBack();
|
||||
});
|
||||
};
|
||||
|
||||
mediaJobUpdate();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 413) {
|
||||
form.controls.file.setValidationResult(
|
||||
false,
|
||||
"The file size is too large",
|
||||
);
|
||||
|
||||
useToastStore().addToast({
|
||||
title: "Error Uploading Media",
|
||||
content: "The file size is too large.",
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
useToastStore().addToast({
|
||||
title: "Error Uploading Media",
|
||||
content: "A server error occurred.",
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
enableFormCallBack();
|
||||
});
|
||||
} else {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
(route.params.id as string).split(",").forEach(async (id) => {
|
||||
try {
|
||||
let data = {
|
||||
title: form.controls.title.value,
|
||||
content: form.controls.content.value,
|
||||
};
|
||||
|
||||
await api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: id,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (errorCount === 0) {
|
||||
useToastStore().addToast({
|
||||
title: "Media Updated",
|
||||
content: `The selected media have been updated.`,
|
||||
type: "success",
|
||||
});
|
||||
} else if (successCount === 0) {
|
||||
useToastStore().addToast({
|
||||
title: "Error Updating Media",
|
||||
content: "An unexpected server error occurred.",
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
useToastStore().addToast({
|
||||
title: "Some Media Updated",
|
||||
content: `Only ${successCount} media items where updated. ${errorCount} could not because of an unexpected error.`,
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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 handleFailValidation = () => {
|
||||
useToastStore().addToast({
|
||||
title: "Save Error",
|
||||
|
||||
Reference in New Issue
Block a user