support editing/deleting multiple items

This commit is contained in:
2023-05-22 16:17:44 +10:00
parent 06b7ce4db0
commit 5d2e9affc0
2 changed files with 297 additions and 84 deletions

View File

@@ -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;

View File

@@ -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>