support editing/deleting multiple items
This commit is contained in:
@@ -14,12 +14,18 @@
|
|||||||
@failed-validation="handleFailValidation">
|
@failed-validation="handleFailValidation">
|
||||||
<SMRow v-if="route.params.id">
|
<SMRow v-if="route.params.id">
|
||||||
<SMColumn class="media-container">
|
<SMColumn class="media-container">
|
||||||
<SMImage :src="imageUrl" />
|
<SMImage v-if="!editMultiple" :src="imageUrl" />
|
||||||
|
<SMImageStack
|
||||||
|
v-if="editMultiple"
|
||||||
|
:src="imageStackUrls" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput control="file" type="file" />
|
<SMInput
|
||||||
|
v-if="!editMultiple"
|
||||||
|
control="file"
|
||||||
|
type="file" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
@@ -30,7 +36,7 @@
|
|||||||
<SMInput control="permission" />
|
<SMInput control="permission" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow v-if="!editMultiple">
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="computedFileSize"
|
v-model="computedFileSize"
|
||||||
@@ -44,7 +50,7 @@
|
|||||||
label="File Mime Type" />
|
label="File Mime Type" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow v-if="!editMultiple">
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="fileData.status"
|
v-model="fileData.status"
|
||||||
@@ -58,7 +64,7 @@
|
|||||||
label="Dimensions" />
|
label="Dimensions" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow v-if="!editMultiple">
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="fileData.url"
|
v-model="fileData.url"
|
||||||
@@ -77,7 +83,7 @@
|
|||||||
<template #right>
|
<template #right>
|
||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Save"
|
:label="editMultiple ? 'Save All' : 'Save'"
|
||||||
:form="form" />
|
:form="form" />
|
||||||
</template>
|
</template>
|
||||||
<template #left>
|
<template #left>
|
||||||
@@ -85,7 +91,9 @@
|
|||||||
:form="form"
|
:form="form"
|
||||||
v-if="route.params.id"
|
v-if="route.params.id"
|
||||||
type="danger"
|
type="danger"
|
||||||
label="Delete"
|
:label="
|
||||||
|
editMultiple ? 'Delete All' : 'Delete'
|
||||||
|
"
|
||||||
@click="handleDelete" />
|
@click="handleDelete" />
|
||||||
</template>
|
</template>
|
||||||
</SMButtonRow>
|
</SMButtonRow>
|
||||||
@@ -111,19 +119,25 @@ import SMForm from "../../components/SMForm.vue";
|
|||||||
import SMInput from "../../components/SMInput.vue";
|
import SMInput from "../../components/SMInput.vue";
|
||||||
import SMMastHead from "../../components/SMMastHead.vue";
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
import SMLoading from "../../components/SMLoading.vue";
|
import SMLoading from "../../components/SMLoading.vue";
|
||||||
import { toTitleCase } from "../../helpers/string";
|
|
||||||
import { useToastStore } from "../../store/ToastStore";
|
import { useToastStore } from "../../store/ToastStore";
|
||||||
import SMColumn from "../../components/SMColumn.vue";
|
import SMColumn from "../../components/SMColumn.vue";
|
||||||
import SMImage from "../../components/SMImage.vue";
|
import SMImage from "../../components/SMImage.vue";
|
||||||
import SMButtonRow from "../../components/SMButtonRow.vue";
|
import SMButtonRow from "../../components/SMButtonRow.vue";
|
||||||
|
import SMImageStack from "../../components/SMImageStack.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const pageError = ref(200);
|
const pageError = ref(200);
|
||||||
const pageLoading = ref(true);
|
const pageLoading = ref(true);
|
||||||
const pageHeading = route.params.id ? "Edit Media" : "Upload Media";
|
const editMultiple = "id" in route.params && route.params.id.includes(",");
|
||||||
|
const pageHeading = route.params.id
|
||||||
|
? editMultiple
|
||||||
|
? "Edit Multiple Media"
|
||||||
|
: "Edit Media"
|
||||||
|
: "Upload Media";
|
||||||
const progressText = ref("");
|
const progressText = ref("");
|
||||||
|
const imageStackUrls = ref([]);
|
||||||
|
|
||||||
const form = reactive(
|
const form = reactive(
|
||||||
Form({
|
Form({
|
||||||
@@ -148,6 +162,7 @@ const imageUrl = ref("");
|
|||||||
|
|
||||||
const handleLoad = async () => {
|
const handleLoad = async () => {
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
|
if (editMultiple === false) {
|
||||||
try {
|
try {
|
||||||
let result = await api.get({
|
let result = await api.get({
|
||||||
url: "/media/{id}",
|
url: "/media/{id}",
|
||||||
@@ -175,6 +190,23 @@ const handleLoad = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
pageError.value = err.status;
|
pageError.value = err.status;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(route.params.id as string).split(",").forEach(async (id) => {
|
||||||
|
try {
|
||||||
|
let result = await api.get({
|
||||||
|
url: "/media/{id}",
|
||||||
|
params: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = result.data as MediaResponse;
|
||||||
|
imageStackUrls.value.push(data.medium.url);
|
||||||
|
} catch (err) {
|
||||||
|
pageError.value = err.status;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageLoading.value = false;
|
pageLoading.value = false;
|
||||||
@@ -183,6 +215,7 @@ const handleLoad = async () => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
form.loading(true);
|
form.loading(true);
|
||||||
|
if (editMultiple === false) {
|
||||||
let submitData = new FormData();
|
let submitData = new FormData();
|
||||||
|
|
||||||
// add file if there is one
|
// add file if there is one
|
||||||
@@ -236,6 +269,51 @@ const handleSubmit = async () => {
|
|||||||
: "The media item been created.",
|
: "The media item been created.",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
|
} 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 urlParams = new URLSearchParams(window.location.search);
|
||||||
const returnUrl = urlParams.get("return");
|
const returnUrl = urlParams.get("return");
|
||||||
@@ -330,7 +408,6 @@ handleLoad();
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-dashboard-media-edit {
|
.page-dashboard-media-edit {
|
||||||
.media-container {
|
.media-container {
|
||||||
max-height: 300px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -36,7 +36,13 @@
|
|||||||
v-else
|
v-else
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:items="items"
|
:items="items"
|
||||||
@row-click="handleEdit">
|
@row-click="handleSelect">
|
||||||
|
<template #item-select="item">
|
||||||
|
<SMInput
|
||||||
|
type="checkbox"
|
||||||
|
v-model="itemsSelected[item.id]"
|
||||||
|
@click.stop />
|
||||||
|
</template>
|
||||||
<template #item-size="item">
|
<template #item-size="item">
|
||||||
{{ bytesReadable(item.size) }}
|
{{ bytesReadable(item.size) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -58,13 +64,28 @@
|
|||||||
"></SMButton>
|
"></SMButton>
|
||||||
</template>
|
</template>
|
||||||
</SMTable>
|
</SMTable>
|
||||||
|
<SMToolbar class="align-items-center">
|
||||||
|
<div>
|
||||||
|
<SMButton
|
||||||
|
type="danger"
|
||||||
|
label="Delete Selected"
|
||||||
|
:disabled="computedSelectedCount == 0"
|
||||||
|
@click="handleDeleteSelected" />
|
||||||
|
<SMButton
|
||||||
|
type="primary"
|
||||||
|
label="Edit Selected"
|
||||||
|
:disabled="computedSelectedCount == 0"
|
||||||
|
@click="handleEditSelected" />
|
||||||
|
</div>
|
||||||
|
<div>{{ computedSelectedCount }} selected</div>
|
||||||
|
</SMToolbar>
|
||||||
</template>
|
</template>
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
@@ -93,8 +114,10 @@ const itemSearch = ref((route.query.search as string) || "");
|
|||||||
const itemsTotal = ref(0);
|
const itemsTotal = ref(0);
|
||||||
const itemsPerPage = 25;
|
const itemsPerPage = 25;
|
||||||
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
|
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
|
||||||
|
const itemsSelected = ref({});
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
|
{ text: "", value: "select", sortable: false },
|
||||||
{ text: "Title (Name)", value: "title", sortable: true },
|
{ text: "Title (Name)", value: "title", sortable: true },
|
||||||
{ text: "Size", value: "size", sortable: true },
|
{ text: "Size", value: "size", sortable: true },
|
||||||
{ text: "Uploaded By", value: "user.display_name", sortable: true },
|
{ text: "Uploaded By", value: "user.display_name", sortable: true },
|
||||||
@@ -178,6 +201,15 @@ const handleLoad = async () => {
|
|||||||
}).relative();
|
}).relative();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
itemsSelected.value,
|
||||||
|
row.id
|
||||||
|
) == false
|
||||||
|
) {
|
||||||
|
itemsSelected.value[row.id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
items.value.push(row);
|
items.value.push(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +228,14 @@ const handleLoad = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelect = (item: Media) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(itemsSelected.value, item.id)) {
|
||||||
|
itemsSelected.value[item.id] = !itemsSelected.value[item.id];
|
||||||
|
} else {
|
||||||
|
itemsSelected.value[item.id] = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User requests to edit the item
|
* User requests to edit the item
|
||||||
*
|
*
|
||||||
@@ -259,6 +299,96 @@ const handleDelete = async (item: Media) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to delete selected media item from the server.
|
||||||
|
*/
|
||||||
|
const handleDeleteSelected = async () => {
|
||||||
|
let result = await openDialog(SMDialogConfirm, {
|
||||||
|
title: "Delete Files?",
|
||||||
|
text: `Are you sure you want to delete the <strong>${computedSelectedCount.value}</strong> selected files?`,
|
||||||
|
cancel: {
|
||||||
|
type: "secondary",
|
||||||
|
label: "Cancel",
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
type: "danger",
|
||||||
|
label: "Delete File",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
let errorCount = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
const deleteItems = Object.entries(itemsSelected.value).filter(
|
||||||
|
([key, value]) => value === true
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
deleteItems.map(async ([key, value]) => {
|
||||||
|
// Perform actions for each item that is true
|
||||||
|
console.log(key, value);
|
||||||
|
|
||||||
|
// Perform asynchronous operation
|
||||||
|
try {
|
||||||
|
await api.delete({
|
||||||
|
url: "/media/{id}",
|
||||||
|
params: {
|
||||||
|
id: key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errorCount === 0) {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Files Deleted",
|
||||||
|
content: `The selected files have been deleted.`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else if (successCount === 0) {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Error Deleting Files",
|
||||||
|
content: "An unexpected server error occurred.",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Some Files Deleted",
|
||||||
|
content: `Only ${successCount} files where deleted. ${errorCount} could not because of an unexpected error.`,
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoad();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to edit selected media item from the server.
|
||||||
|
*/
|
||||||
|
const handleEditSelected = async () => {
|
||||||
|
const editItems = Object.entries(itemsSelected.value)
|
||||||
|
.filter(([key, value]) => value === true)
|
||||||
|
.map(([key, value]) => key)
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: "dashboard-media-edit",
|
||||||
|
params: { id: editItems },
|
||||||
|
query: {
|
||||||
|
return: encodeURIComponent(
|
||||||
|
window.location.pathname + window.location.search
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the user requesting to download the item.
|
* Handle the user requesting to download the item.
|
||||||
*
|
*
|
||||||
@@ -268,6 +398,12 @@ const handleDownload = (item: Media) => {
|
|||||||
window.open(`${item.url}?download=1`, "_blank");
|
window.open(`${item.url}?download=1`, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const computedSelectedCount = computed(() => {
|
||||||
|
const selectedValues = Object.values(itemsSelected.value);
|
||||||
|
const trueValues = selectedValues.filter((value) => value === true);
|
||||||
|
return trueValues.length;
|
||||||
|
});
|
||||||
|
|
||||||
handleLoad();
|
handleLoad();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user