Files
Website/resources/js/views/dashboard/MediaEdit.vue

577 lines
20 KiB
Vue

<template>
<SMPageStatus v-if="!userHasPermission('admin/media')" :status="403" />
<template v-else>
<SMMastHead
:title="pageHeading"
:back-link="{ name: 'dashboard-media-list' }"
back-title="Back to Media" />
<SMLoading v-if="form.loading()" />
<div v-else class="max-w-4xl mx-auto px-4 mt-8">
<SMForm
:model-value="form"
@submit="handleSubmit"
@failed-validation="handleFailValidation">
<div>
<SMImageGallery class="mb-4" :model-value="galleryItems" />
</div>
<SMSelectFile
v-if="!editMultiple"
control="file"
upload-only
accepts="*"
class="mb-4" />
<SMInput control="title" class="mb-4" />
<div class="flex flex-col md:flex-row gap-4">
<SMDropdown
class="mb-4"
control="security_type"
type="select"
:options="{
'': 'None',
permission: 'Permission',
password: 'Password',
}" />
<SMInput
v-if="form.controls.security_type.value != ''"
class="mb-4"
control="security_data"
:label="
toTitleCase(
form.controls.security_type.value.toString(),
)
" />
</div>
<div
v-if="!editMultiple"
class="flex flex-col md:flex-row gap-4">
<SMInput
class="mb-4"
v-model="computedFileSize"
disabled
label="File Size" />
<SMInput
class="mb-4"
v-model="fileData.mime_type"
disabled
label="File Mime Type" />
</div>
<div
v-if="!editMultiple"
class="flex flex-col md:flex-row gap-4">
<SMInput
class="mb-4"
v-model="fileData.status"
disabled
label="Status" />
<SMInput
class="mb-4"
v-model="fileData.dimensions"
disabled
label="Dimensions" />
</div>
<SMInput
v-if="!editMultiple"
class="mb-4"
v-model="fileData.url"
disabled
label="URL" />
<SMInput class="mb-4" textarea control="description" />
<div class="flex flex-justify-end gap-4">
<button
v-if="route.params.id"
type="button"
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-red-600 hover:bg-red-500 text-white cursor-pointer"
@click="handleDelete">
{{ editMultiple ? "Delete All" : "Delete" }}
</button>
<input
role="button"
type="submit"
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"
:value="editMultiple ? 'Save All' : 'Save'" />
</div>
</SMForm>
</div>
</template>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ApiOptions, api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";
import { bytesReadable } from "../../helpers/types";
import { And, Required } from "../../helpers/validate";
import {
Media,
MediaJobResponse,
MediaResponse,
} from "../../helpers/api.types";
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";
import SMDropdown from "../../components/SMDropdown.vue";
import SMMastHead from "../../components/SMMastHead.vue";
import SMLoading from "../../components/SMLoading.vue";
import { useToastStore } from "../../store/ToastStore";
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";
import SMDialogProgress from "../../components/dialogs/SMDialogProgress.vue";
import { mediaGetWebURL } from "../../helpers/media";
const route = useRoute();
const router = useRouter();
const pageError = ref(200);
const editMultiple = "id" in route.params && route.params.id.includes(",");
const pageHeading = route.params.id
? editMultiple
? "Edit Multiple Media"
: "Edit Media"
: "Upload Media";
const galleryItems = ref([]);
const form = reactive(
Form({
file: FormControl("", And([Required()])),
title: FormControl("", Required()),
description: FormControl(),
security_type: FormControl(),
security_data: FormControl(),
}),
);
const fileData = reactive({
url: "Not available",
mime_type: "--",
size: 0,
storage: "--",
status: "--",
dimensions: "--",
user: {},
});
const imageUrl = ref("");
const handleLoad = async () => {
if (route.params.id) {
if (editMultiple === false) {
try {
form.loading(true);
let result = await api.get({
url: "/media/{id}",
params: {
id: route.params.id,
},
});
const data = result.data as MediaResponse;
form.controls.file.value = data.medium;
form.controls.title.value = data.medium.title;
form.controls.description.value = data.medium.description;
form.controls.security_type.value = data.medium.security_type;
form.controls.security_data.value = data.medium.security_data;
fileData.url = mediaGetWebURL(data.medium);
fileData.mime_type = data.medium.mime_type;
fileData.size = data.medium.size;
fileData.storage = data.medium.storage;
fileData.status =
data.medium.status == "" ? "OK" : data.medium.status;
fileData.dimensions = data.medium.dimensions;
imageUrl.value = fileData.url;
} catch (err) {
pageError.value = err.status;
} finally {
form.loading(false);
}
} else {
(route.params.id as string).split(",").forEach(async (id) => {
try {
form.loading(true);
let result = await api.get({
url: "/media/{id}",
params: {
id: id,
},
});
const data = result.data as MediaResponse;
galleryItems.value.push(data.medium);
} catch (err) {
pageError.value = err.status;
} finally {
form.loading(false);
}
});
}
}
};
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;
}
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(
"security_type",
form.controls.security_type.value as string,
);
submitData.append(
"security_data",
form.controls.security_type.value == ""
? ""
: (form.controls.security_data.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/jobs/{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 handleFailValidation = () => {
useToastStore().addToast({
title: "Save Error",
content:
"There are some errors in the form. Fix these before continuing.",
type: "danger",
});
};
const handleDelete = async () => {
let result = await openDialog(DialogConfirm, {
title: "Delete File?",
text: `Are you sure you want to delete the file <strong>${form.controls.title.value}</strong>?`,
cancel: {
type: "secondary",
label: "Cancel",
},
confirm: {
type: "danger",
label: "Delete File",
},
});
if (result) {
try {
await api.delete({
url: "/media/{id}",
params: {
id: route.params.id,
},
});
router.push({ name: "media" });
} catch (error) {
useToastStore().addToast({
title: "Error Deleting File",
content:
error.data?.message ||
"An unexpected server error occurred",
type: "danger",
});
}
}
};
const computedFileSize = computed(() => {
if (isNaN(+fileData.size) == true) {
return fileData.size;
}
return bytesReadable(fileData.size);
});
watch(
() => form.controls.file.value,
(newValue) => {
if (typeof newValue === "object" && newValue !== null) {
if ("type" in newValue && typeof newValue.type === "string") {
fileData.mime_type = newValue.type;
} else if (
"mime_type" in newValue &&
typeof newValue.mime_type === "string"
) {
fileData.mime_type = newValue.mime_type;
}
if ("size" in newValue && typeof newValue.size === "number") {
fileData.size = newValue.size;
}
}
fileData.mime_type =
(newValue as File).type || (newValue as Media).mime_type;
fileData.size = (newValue as File).size;
if ((form.controls.title.value as string).length == 0) {
form.controls.title.value = (newValue as File).name
.replace(/\.[^/.]+$/, "")
.replace(/[^\w\s]/g, " ")
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
}
},
);
handleLoad();
</script>
<style lang="scss">
.page-dashboard-media-edit {
.media-container {
display: flex;
justify-content: center;
align-items: center;
}
}
</style>