From b2a4f37425bbbcf8df1ae91d00b4bdedca7ab836 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Tue, 16 Jan 2024 13:55:24 +0100
Subject: [PATCH] Add update resource modal

#88
---
 src/components/NavbarTop.vue                  |   1 +
 src/components/resources/ResourceCard.vue     |  18 ++-
 .../{ => modals}/CreateResourceModal.vue      |   0
 .../resources/modals/UpdateResourceModal.vue  | 129 ++++++++++++++++++
 .../{ => modals}/UploadResourceInfoModal.vue  |   0
 src/stores/resources.ts                       |  20 +++
 src/views/resources/MyResourcesView.vue       |  26 +++-
 7 files changed, 189 insertions(+), 5 deletions(-)
 rename src/components/resources/{ => modals}/CreateResourceModal.vue (100%)
 create mode 100644 src/components/resources/modals/UpdateResourceModal.vue
 rename src/components/resources/{ => modals}/UploadResourceInfoModal.vue (100%)

diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue
index 60447d3..57d8e84 100644
--- a/src/components/NavbarTop.vue
+++ b/src/components/NavbarTop.vue
@@ -241,6 +241,7 @@ watch(
     modal-i-d="advancedUsageModal"
     modal-label="Advanced Usage Modal"
     v-if="store.authenticated"
+    size-modifier="lg"
   >
     <template v-slot:header>
       <h3>Advanced Usage</h3>
diff --git a/src/components/resources/ResourceCard.vue b/src/components/resources/ResourceCard.vue
index ffcedbd..c4bca17 100644
--- a/src/components/resources/ResourceCard.vue
+++ b/src/components/resources/ResourceCard.vue
@@ -27,12 +27,15 @@ let refreshTimeout: NodeJS.Timeout | undefined = undefined;
 
 const emit = defineEmits<{
   (e: "click-info", resourceVersion: ResourceVersionOut): void;
+  (e: "click-update", resource: ResourceOut): void;
 }>();
 
 const resourceVersionS3Ready = ref<Record<string, boolean>>({});
 
-const resourceVersions = computed<ResourceVersionOut[]>(
-  () => props.resource.versions,
+const resourceVersions = computed<ResourceVersionOut[]>(() =>
+  [...props.resource.versions].sort((a, b) =>
+    a.created_at < b.created_at ? 1 : -1,
+  ),
 );
 
 function checkS3Resource(resourceVersion: ResourceVersionOut) {
@@ -92,6 +95,17 @@ onMounted(() => {
         <div v-else>
           <span>{{ props.resource.name }}</span>
         </div>
+        <button
+          v-if="props.extended"
+          :disabled="props.loading"
+          class="btn btn-primary"
+          type="button"
+          data-bs-toggle="modal"
+          data-bs-target="#updateResourceModal"
+          @click="emit('click-update', props.resource)"
+        >
+          Update
+        </button>
       </div>
       <p class="card-text">
         <span v-if="props.loading" class="placeholder-glow"
diff --git a/src/components/resources/CreateResourceModal.vue b/src/components/resources/modals/CreateResourceModal.vue
similarity index 100%
rename from src/components/resources/CreateResourceModal.vue
rename to src/components/resources/modals/CreateResourceModal.vue
diff --git a/src/components/resources/modals/UpdateResourceModal.vue b/src/components/resources/modals/UpdateResourceModal.vue
new file mode 100644
index 0000000..f40a8d1
--- /dev/null
+++ b/src/components/resources/modals/UpdateResourceModal.vue
@@ -0,0 +1,129 @@
+<script setup lang="ts">
+import { reactive, onMounted, ref } from "vue";
+import BootstrapModal from "@/components/modals/BootstrapModal.vue";
+import { Modal } from "bootstrap";
+import { useResourceStore } from "@/stores/resources";
+import type { ResourceVersionIn, ResourceOut } from "@/client/resource";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import { Tooltip } from "bootstrap";
+
+const resourceRepository = useResourceStore();
+
+const resourceUpdateForm = ref<HTMLFormElement | undefined>(undefined);
+
+const resourceUpdate = reactive<ResourceVersionIn>({
+  release: "",
+});
+
+const formState = reactive<{
+  validated: boolean;
+  loading: boolean;
+}>({
+  validated: false,
+  loading: false,
+});
+
+const props = defineProps<{
+  modalId: string;
+  resource: ResourceOut;
+}>();
+
+let updateResourceModal: Modal | null = null;
+
+function updateResource() {
+  formState.validated = true;
+  resourceUpdate.release = resourceUpdate.release.trim();
+  if (resourceUpdateForm.value?.checkValidity()) {
+    formState.loading = true;
+    resourceRepository
+      .updateResource(props.resource.resource_id, resourceUpdate)
+      .then(() => {
+        updateResourceModal?.hide();
+        resourceUpdate.release = "";
+        formState.validated = false;
+      })
+      .finally(() => {
+        formState.loading = false;
+      });
+  }
+}
+
+function modalClosed() {
+  formState.validated = false;
+}
+
+onMounted(() => {
+  updateResourceModal = new Modal("#" + props.modalId);
+  new Tooltip("#tooltip-update-resource-release");
+});
+</script>
+
+<template>
+  <bootstrap-modal
+    :modalID="modalId"
+    static-backdrop
+    modal-label="Update Resource Modal"
+    v-on="{ 'hidden.bs.modal': modalClosed }"
+  >
+    <template #header>
+      Update Resource <b>{{ props.resource.name }}</b></template
+    >
+    <template #body>
+      <form
+        id="resourceUpdateForm"
+        :class="{ 'was-validated': formState.validated }"
+        ref="resourceUpdateForm"
+      >
+        <div class="mb-3">
+          <label for="resourceUpdateReleaseInput" class="form-label">
+            Release
+          </label>
+          <div class="input-group">
+            <div class="input-group-text">
+              <font-awesome-icon icon="fa-solid fa-tag" />
+            </div>
+            <input
+              class="form-control"
+              id="resourceUpdateReleaseInput"
+              required
+              minlength="3"
+              maxlength="32"
+              v-model="resourceUpdate.release"
+              placeholder="Next release name"
+            />
+            <div
+              class="input-group-text hover-info"
+              id="tooltip-update-resource-release"
+              data-bs-toggle="tooltip"
+              data-bs-title="The name of the next resource version"
+            >
+              <font-awesome-icon icon="fa-solid fa-circle-question" />
+            </div>
+          </div>
+        </div>
+      </form>
+    </template>
+    <template v-slot:footer>
+      <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
+        Close
+      </button>
+      <button
+        type="submit"
+        form="resourceUpdateForm"
+        class="btn btn-primary"
+        :disabled="formState.loading || !props.resource.resource_id"
+        @click.prevent="updateResource"
+      >
+        <span
+          v-if="formState.loading"
+          class="spinner-border spinner-border-sm"
+          role="status"
+          aria-hidden="true"
+        ></span>
+        Save
+      </button>
+    </template>
+  </bootstrap-modal>
+</template>
+
+<style scoped></style>
diff --git a/src/components/resources/UploadResourceInfoModal.vue b/src/components/resources/modals/UploadResourceInfoModal.vue
similarity index 100%
rename from src/components/resources/UploadResourceInfoModal.vue
rename to src/components/resources/modals/UploadResourceInfoModal.vue
diff --git a/src/stores/resources.ts b/src/stores/resources.ts
index 49516a1..40aa0cf 100644
--- a/src/stores/resources.ts
+++ b/src/stores/resources.ts
@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
 import type {
   ResourceIn,
   ResourceOut,
+  ResourceVersionIn,
   ResourceVersionOut,
 } from "@/client/resource";
 import { ResourceService, ResourceVersionService } from "@/client/resource";
@@ -96,6 +97,7 @@ export const useResourceStore = defineStore({
           undefined
         ) {
           this.fetchOwnResource(resourceVersion.resource_id);
+          return changedResourceVersion;
         }
         const versionIndex = this.ownResourceMapping[
           changedResourceVersion.resource_id
@@ -116,5 +118,23 @@ export const useResourceStore = defineStore({
         return changedResourceVersion;
       });
     },
+    updateResource(
+      resource_id: string,
+      version: ResourceVersionIn,
+    ): Promise<ResourceVersionOut> {
+      return ResourceVersionService.resourceVersionRequestResourceVersion(
+        resource_id,
+        version,
+      ).then((versionOut) => {
+        if (this.ownResourceMapping[versionOut.resource_id] == undefined) {
+          this.fetchOwnResource(versionOut.resource_id);
+          return versionOut;
+        }
+        this.ownResourceMapping[versionOut.resource_id].versions.push(
+          versionOut,
+        );
+        return versionOut;
+      });
+    },
   },
 });
diff --git a/src/views/resources/MyResourcesView.vue b/src/views/resources/MyResourcesView.vue
index 11dcacb..e9a3d23 100644
--- a/src/views/resources/MyResourcesView.vue
+++ b/src/views/resources/MyResourcesView.vue
@@ -3,11 +3,12 @@ import { onMounted, reactive } from "vue";
 import { useResourceStore } from "@/stores/resources";
 import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue";
 import ResourceCard from "@/components/resources/ResourceCard.vue";
-import CreateResourceModal from "@/components/resources/CreateResourceModal.vue";
-import UploadResourceInfoModal from "@/components/resources/UploadResourceInfoModal.vue";
+import CreateResourceModal from "@/components/resources/modals/CreateResourceModal.vue";
+import UploadResourceInfoModal from "@/components/resources/modals/UploadResourceInfoModal.vue";
 import { useS3KeyStore } from "@/stores/s3keys";
-import type { ResourceVersionOut } from "@/client/resource";
+import type { ResourceVersionOut, ResourceOut } from "@/client/resource";
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import UpdateResourceModal from "@/components/resources/modals/UpdateResourceModal.vue";
 
 const resourceRepository = useResourceStore();
 const s3KeyRepository = useS3KeyStore();
@@ -15,15 +16,28 @@ const s3KeyRepository = useS3KeyStore();
 const resourceState = reactive<{
   loading: boolean;
   resourceVersionInfo?: ResourceVersionOut;
+  updateResource: ResourceOut;
 }>({
   loading: true,
   resourceVersionInfo: undefined,
+  updateResource: {
+    name: "",
+    description: "",
+    source: "",
+    resource_id: "",
+    versions: [],
+    maintainer_id: "",
+  },
 });
 
 function setResourceVersionInfo(resourceVersionInfo?: ResourceVersionOut) {
   resourceState.resourceVersionInfo = resourceVersionInfo;
 }
 
+function setResourceUpdate(resource: ResourceOut) {
+  resourceState.updateResource = resource;
+}
+
 onMounted(() => {
   let fetchedResources = false;
   s3KeyRepository.fetchS3Keys(() => {
@@ -43,6 +57,10 @@ onMounted(() => {
     modal-id="uploadResourceInfoModal"
     :resource-version="resourceState.resourceVersionInfo"
   />
+  <update-resource-modal
+    :resource="resourceState.updateResource"
+    modal-id="updateResourceModal"
+  />
   <div
     class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2"
   >
@@ -90,6 +108,7 @@ onMounted(() => {
         style="width: 48%"
         extended
         @click-info="setResourceVersionInfo"
+        @click-update="setResourceUpdate"
       />
     </CardTransitionGroup>
   </div>
@@ -110,6 +129,7 @@ onMounted(() => {
       }"
       style="min-width: 48%"
       loading
+      extended
     />
   </div>
 </template>
-- 
GitLab