From 4d07ae0a1ecc0f2fe72dc0c42c6eed3d2ac60e5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Tue, 27 Sep 2022 10:44:32 +0200
Subject: [PATCH] Add modal to confirm deletion

---
 src/components/BucketView.vue             | 30 ++++++-
 src/components/Modals/DeleteModal.vue     | 95 +++++++++++++++++++++++
 src/components/Modals/PermissionModal.vue | 15 +++-
 src/views/object-storage/BucketsView.vue  | 20 +++++
 4 files changed, 156 insertions(+), 4 deletions(-)
 create mode 100644 src/components/Modals/DeleteModal.vue

diff --git a/src/components/BucketView.vue b/src/components/BucketView.vue
index 819a2a2..b43e2c3 100644
--- a/src/components/BucketView.vue
+++ b/src/components/BucketView.vue
@@ -17,6 +17,7 @@ import CopyObjectModal from "@/components/Modals/CopyObjectModal.vue";
 import PermissionModal from "@/components/Modals/PermissionModal.vue";
 import ObjectDetailModal from "@/components/Modals/ObjectDetailModal.vue";
 import CreateFolderModal from "@/components/Modals/CreateFolderModal.vue";
+import DeleteModal from "@/components/Modals/DeleteModal.vue"
 import {
   S3Client,
   DeleteObjectCommand,
@@ -121,6 +122,8 @@ const objectState = reactive({
   createdPermission: undefined,
   deletedItem: "",
   editObjectKey: "",
+  potentialObjectToDelete: "",
+  deleteFolder: true,
   copyObject: {
     key: "",
     size: 0,
@@ -141,6 +144,8 @@ const objectState = reactive({
   createdPermission: undefined | BucketPermission;
   deletedItem: string;
   editObjectKey: string;
+  potentialObjectToDelete: string;
+  deleteFolder: boolean;
   copyObject: S3ObjectMetaInformation;
   viewDetailObject: S3ObjectMetaInformation;
 });
@@ -388,11 +393,16 @@ function objectCopied(copiedObject: S3ObjectMetaInformation) {
   }
 }
 
+function deleteObject(key: string) {
+  objectState.potentialObjectToDelete = key;
+  objectState.deleteFolder = false;
+}
+
 /**
  * Delete an Object in the current folder
  * @param key Key of the Object
  */
-function deleteObject(key: string) {
+function confirmedDeleteObject(key: string) {
   const command = new DeleteObjectCommand({
     Bucket: props.bucketName,
     Key: key,
@@ -432,11 +442,16 @@ async function downloadObject(key: string, bucket: string) {
   document.body.removeChild(element);
 }
 
+function deleteFolder(folderPath: string) {
+  objectState.potentialObjectToDelete = folderPath;
+  objectState.deleteFolder = true;
+}
+
 /**
  * Delete a folder in the current Bucket
  * @param folderPath Path to the folder with a trailing "/", e.g. some/path/to/a/folder/
  */
-function deleteFolder(folderPath: string) {
+function confirmedDeleteFolder(folderPath: string) {
   const command = new DeleteObjectsCommand({
     Bucket: props.bucketName,
     Delete: {
@@ -506,6 +521,13 @@ watch(
       </div>
     </div>
   </div>
+  <DeleteModal
+    modalID="delete-object-modal"
+    modal-label="some-label"
+    :object-name-delete="objectState.potentialObjectToDelete"
+    :back-modal-id="undefined"
+    @confirm-delete="objectState.deleteFolder ? confirmedDeleteFolder(objectState.potentialObjectToDelete) : confirmedDeleteObject(objectState.potentialObjectToDelete)"
+  />
   <!-- Navbar Breadcrumb -->
   <nav aria-label="breadcrumb" class="fs-2">
     <ol class="breadcrumb">
@@ -805,6 +827,8 @@ watch(
                       class="dropdown-item text-danger align-middle"
                       type="button"
                       @click="deleteObject(obj.key)"
+                      data-bs-toggle="modal"
+                      data-bs-target="#delete-object-modal"
                       :disabled="!writeS3Permission"
                     >
                       <bootstrap-icon
@@ -825,6 +849,8 @@ watch(
                   type="button"
                   class="btn btn-danger btn-sm align-middle"
                   :disabled="!writeS3Permission"
+                  data-bs-toggle="modal"
+                  data-bs-target="#delete-object-modal"
                   @click="
                     deleteFolder(
                       obj.parentFolder.concat(['']).join('/') + obj.name + '/'
diff --git a/src/components/Modals/DeleteModal.vue b/src/components/Modals/DeleteModal.vue
new file mode 100644
index 0000000..108993d
--- /dev/null
+++ b/src/components/Modals/DeleteModal.vue
@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import { onMounted, ref } from "vue";
+import type { Ref } from "vue";
+import { Modal } from "bootstrap";
+import BootstrapModal from "@/components/Modals/BootstrapModal.vue";
+
+const props = defineProps<{
+  modalID: string;
+  modalLabel: string;
+  objectNameDelete: string;
+  backModalId: string | undefined;
+}>();
+
+const confirmDelete: Ref<boolean> = ref(false);
+const emit = defineEmits<{
+  (e: "confirm-delete"): void;
+}>();
+
+let deleteModal: Modal | null = null;
+const randomIDSuffix = Math.random().toString(16).substr(2, 8);
+
+function modalClosed() {
+  confirmDelete.value = false;
+}
+
+function deleteObject() {
+  emit("confirm-delete");
+  deleteModal?.hide();
+  confirmDelete.value = false;
+}
+
+onMounted(() => {
+  deleteModal = Modal.getOrCreateInstance("#" + props.modalID);
+});
+</script>
+
+<template>
+  <bootstrap-modal
+    :modalID="props.modalID"
+    :static-backdrop="true"
+    :modal-label="props.modalLabel"
+    v-on="{ 'hidden.bs.modal': modalClosed }"
+  >
+    <template v-slot:header> Delete {{ props.objectNameDelete }}</template>
+    <template v-slot:body>
+      <p>
+        Are you sure you want to delete
+        <strong>{{ props.objectNameDelete }}</strong>
+      </p>
+      <div class="form-check">
+        <input
+          class="form-check-input"
+          type="checkbox"
+          v-model="confirmDelete"
+          :id="'checkConfirmDelete' + randomIDSuffix"
+        />
+        <label
+          class="form-check-label"
+          :for="'checkConfirmDelete' + randomIDSuffix"
+        >
+          <strong>Yes</strong>, I am sure.
+        </label>
+      </div>
+    </template>
+    <template v-slot:footer>
+      <button
+        v-if="backModalId !== undefined"
+        type="button"
+        class="btn btn-secondary"
+        :data-bs-target="'#' + props.backModalId"
+        data-bs-toggle="modal"
+      >
+        Back
+      </button>
+      <button
+        v-else
+        type="button"
+        class="btn btn-secondary"
+        data-bs-dismiss="modal"
+      >
+        Close
+      </button>
+      <button
+        type="button"
+        class="btn btn-danger"
+        :disabled="!confirmDelete"
+        @click="deleteObject"
+      >
+        Delete
+      </button>
+    </template>
+  </bootstrap-modal>
+</template>
+
+<style scoped></style>
diff --git a/src/components/Modals/PermissionModal.vue b/src/components/Modals/PermissionModal.vue
index cafe98c..a470e2f 100644
--- a/src/components/Modals/PermissionModal.vue
+++ b/src/components/Modals/PermissionModal.vue
@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { onMounted, reactive, watch, ref, computed } from "vue";
 import BootstrapModal from "@/components/Modals/BootstrapModal.vue";
+import DeleteModal from "@/components/Modals/DeleteModal.vue";
 import { Modal } from "bootstrap";
 import dayjs from "dayjs";
 import type {
@@ -248,7 +249,7 @@ function formSubmit() {
  * @param bucketName Bucket to delete
  * @param uid ID of grantee of the permission
  */
-function deletePermission(bucketName: string, uid: string) {
+function confirmedDeletePermission(bucketName: string, uid: string) {
   if (!formState.loading) {
     formState.loading = true;
     BucketPermissionsService.bucketPermissionsDeletePermissionForBucket(
@@ -280,6 +281,15 @@ onMounted(() => {
 </script>
 
 <template>
+  <DeleteModal
+    :modalID="'delete-permission-modal' + randomIDSuffix"
+    modal-label="some-label"
+    object-name-delete="permission"
+    :back-modal-id="modalID"
+    @confirm-delete="
+      confirmedDeletePermission(permission.bucket_name, permission.uid)
+    "
+  />
   <div class="toast-container position-fixed top-0 end-0 p-3">
     <div
       role="alert"
@@ -329,7 +339,8 @@ onMounted(() => {
         fill="currentColor"
         class="me-2"
         :class="{ 'delete-icon': !formState.loading }"
-        @click="deletePermission(permission.bucket_name, permission.uid)"
+        data-bs-toggle="modal"
+        :data-bs-target="'#delete-permission-modal' + randomIDSuffix"
       />
       <bootstrap-icon
         v-if="formState.readonly && props.editable"
diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue
index c01d35e..58422c9 100644
--- a/src/views/object-storage/BucketsView.vue
+++ b/src/views/object-storage/BucketsView.vue
@@ -6,8 +6,10 @@ import { BucketPermissionsService, BucketService } from "@/client";
 import { useRoute, useRouter } from "vue-router";
 import BootstrapIcon from "@/components/BootstrapIcon.vue";
 import CreateBucketModal from "@/components/Modals/CreateBucketModal.vue";
+import DeleteModal from "@/components/Modals/DeleteModal.vue";
 import BucketListItem from "@/components/BucketListItem.vue";
 import { useAuthStore } from "@/stores/auth";
+import { Modal } from "bootstrap";
 
 const route = useRoute();
 const router = useRouter();
@@ -16,12 +18,15 @@ const authStore = useAuthStore();
 const bucketsState = reactive({
   buckets: [],
   permissions: {},
+  potentialDeleteBucketName: "",
   loading: true,
 } as {
   buckets: BucketOut[];
   loading: boolean;
   permissions: Record<string, BucketPermission>;
+  potentialDeleteBucketName: string;
 });
+let deleteModal: Modal | null = null;
 
 function fetchBuckets() {
   BucketService.bucketListBuckets()
@@ -63,6 +68,11 @@ function addBucket(bucket: BucketOut) {
 }
 
 function deleteBucket(bucketName: string) {
+  bucketsState.potentialDeleteBucketName = bucketName;
+  deleteModal?.show();
+}
+
+function confirmedDeleteBucket(bucketName: string) {
   BucketService.bucketDeleteBucket(bucketName, true).then(() => {
     bucketDeleted(bucketName);
   });
@@ -78,11 +88,21 @@ function bucketDeleted(bucketName: string) {
 }
 
 onMounted(() => {
+  deleteModal = Modal.getOrCreateInstance("#delete-bucket-modal");
   fetchBuckets();
 });
 </script>
 
 <template>
+  <DeleteModal
+    modalID="delete-bucket-modal"
+    modal-label="some-label"
+    :object-name-delete="bucketsState.potentialDeleteBucketName"
+    :back-modal-id="undefined"
+    @confirm-delete="
+      confirmedDeleteBucket(bucketsState.potentialDeleteBucketName)
+    "
+  />
   <div class="row m-2 border-bottom border-light mt-4">
     <div class="col-12"></div>
     <h1 class="mb-2 text-light">Buckets</h1>
-- 
GitLab