From 26f95683e79bf97953053154eac9894bb5c97512 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Thu, 21 Mar 2024 14:54:44 +0100
Subject: [PATCH] Add button to delete a parameter extension

#105
---
 src/App.vue                                   |  48 +++--
 .../services/WorkflowVersionService.ts        |  33 +++-
 src/stores/workflows.ts                       |  46 ++++-
 .../CreateParameterTranslationView.vue        | 173 ++++++++++++++----
 4 files changed, 253 insertions(+), 47 deletions(-)

diff --git a/src/App.vue b/src/App.vue
index 0ba8e44..72b4a33 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { onBeforeMount } from "vue";
+import { onBeforeMount, onMounted } from "vue";
 import { useCookies } from "vue3-cookies";
 import { useAuthStore } from "@/stores/users";
 import { useRoute, useRouter } from "vue-router";
@@ -12,12 +12,20 @@ import axios from "axios";
 import { useNameStore } from "@/stores/names";
 import AppHeader from "@/components/AppHeader.vue";
 import AppFooter from "@/components/AppFooter.vue";
+import { useResourceStore } from "@/stores/resources";
+import { useWorkflowStore } from "@/stores/workflows";
+import { useBucketStore } from "@/stores/buckets";
+import {useS3KeyStore} from "@/stores/s3keys";
 
 const { cookies } = useCookies();
-const store = useAuthStore();
 const router = useRouter();
 const route = useRoute();
+const userRepository = useAuthStore();
 const nameRepository = useNameStore();
+const resourceRepository = useResourceStore();
+const workflowRepository = useWorkflowStore();
+const bucketRepository = useBucketStore();
+const s3KeyRepository = useS3KeyStore();
 
 onBeforeMount(() => {
   S3ProxyOpenAPI.BASE = environment.S3PROXY_API_BASE_URL;
@@ -31,7 +39,7 @@ onBeforeMount(() => {
         (err.response.status === 400 || err.response.status === 403) &&
         err.response.data.detail?.includes("JWT")
       ) {
-        store.logout();
+        userRepository.logout();
         cookies.remove("bearer");
         router.push({
           name: "login",
@@ -46,15 +54,15 @@ onBeforeMount(() => {
       return Promise.reject(err);
     },
   );
-  store.setToken(cookies.get("bearer"));
+  userRepository.setToken(cookies.get("bearer"));
   router.afterEach((to, from) => {
     window._paq.push(["setReferrerUrl", from.path]);
     window._paq.push(["deleteCustomVariables", "page"]);
     window._paq.push(["deleteCustomDimension", 1]);
     window._paq.push(["setCustomUrl", to.path]);
     window._paq.push(["setDocumentTitle", to.name]);
-    if (store.currentUID.length > 0) {
-      window._paq.push(["setUserId", store.currentUID]);
+    if (userRepository.currentUID.length > 0) {
+      window._paq.push(["setUserId", userRepository.currentUID]);
     }
     window._paq.push(["trackPageView"]);
     window._paq.push(["enableLinkTracking"]);
@@ -62,7 +70,7 @@ onBeforeMount(() => {
   router.beforeEach(async (to) => {
     // make sure the user is authenticated
     if (
-      !store.authenticated &&
+      !userRepository.authenticated &&
       // ❗️ Avoid an infinite redirect
       to.name !== "login"
     ) {
@@ -73,20 +81,20 @@ onBeforeMount(() => {
       };
     } else if (
       to.meta.requiresDeveloperRole &&
-      !(store.workflowDev || store.admin)
+      !(userRepository.workflowDev || userRepository.admin)
     ) {
       return { name: "dashboard" };
     } else if (
       to.meta.requiresReviewerRole &&
-      !(store.rewiewer || store.admin)
+      !(userRepository.rewiewer || userRepository.admin)
     ) {
       return { name: "dashboard" };
     } else if (
       to.meta.requiresMaintainerRole &&
-      !(store.resourceMaintainer || store.admin)
+      !(userRepository.resourceMaintainer || userRepository.admin)
     ) {
       return { name: "dashboard" };
-    } else if (to.meta.adminRole && !store.admin) {
+    } else if (to.meta.adminRole && !userRepository.admin) {
       return { name: "dashboard" };
     } else if (to.name !== "login" && to.query.return_path) {
       // return to original path after login
@@ -95,6 +103,24 @@ onBeforeMount(() => {
   });
   nameRepository.loadNameMapping();
 });
+
+onMounted(() => {
+  if (userRepository.authenticated) {
+    resourceRepository.fetchPublicResources();
+    workflowRepository.fetchWorkflows();
+    bucketRepository.fetchBuckets();
+    s3KeyRepository.fetchS3Keys();
+    if (!userRepository.foreignUser) {
+      bucketRepository.fetchOwnPermissions();
+    }
+    if (userRepository.workflowDev || userRepository.admin) {
+      workflowRepository.fetchOwnWorkflows();
+    }
+    if (userRepository.resourceMaintainer || userRepository.admin) {
+      resourceRepository.fetchOwnResources();
+    }
+  }
+});
 </script>
 
 <template>
diff --git a/src/client/workflow/services/WorkflowVersionService.ts b/src/client/workflow/services/WorkflowVersionService.ts
index b247d41..289e210 100644
--- a/src/client/workflow/services/WorkflowVersionService.ts
+++ b/src/client/workflow/services/WorkflowVersionService.ts
@@ -142,7 +142,7 @@ export class WorkflowVersionService {
         });
     }
     /**
-     * Deprecate a workflow version
+     * Update parameter extension of workflow version
      * Update the parameter extension of a workflow version.
      *
      *
@@ -176,6 +176,37 @@ export class WorkflowVersionService {
             },
         });
     }
+    /**
+     * Delete parameter extension of workflow version
+     * Delete the parameter extension of a workflow version.
+     *
+     *
+     * Permission `workflow:update` required.
+     * @param wid ID of a workflow
+     * @param gitCommitHash Git commit git_commit_hash of specific version.
+     * @returns void
+     * @throws ApiError
+     */
+    public static workflowVersionDeleteWorkflowVersionParameterExtension(
+        wid: string,
+        gitCommitHash: string,
+    ): CancelablePromise<void> {
+        return __request(OpenAPI, {
+            method: 'DELETE',
+            url: '/workflows/{wid}/versions/{git_commit_hash}/parameter-extension',
+            path: {
+                'wid': wid,
+                'git_commit_hash': gitCommitHash,
+            },
+            errors: {
+                400: `Error decoding JWT Token`,
+                401: `Not authenticated`,
+                403: `Not authorized`,
+                404: `Entity not Found`,
+                422: `Validation Error`,
+            },
+        });
+    }
     /**
      * Fetch documentation for a workflow version
      * Get the documentation for a specific workflow version.
diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts
index 8f1b70c..b00d127 100644
--- a/src/stores/workflows.ts
+++ b/src/stores/workflows.ts
@@ -555,8 +555,7 @@ export const useWorkflowStore = defineStore({
             (version) => version.workflow_version_id == version_id,
           );
           if (versionIndex1 > -1) {
-            this.workflowMapping[workflow_id].versions[versionIndex1].icon_url =
-              null;
+            this.workflowMapping[workflow_id].versions[versionIndex1] = version;
           }
         }
 
@@ -572,11 +571,52 @@ export const useWorkflowStore = defineStore({
           if (versionIndex2 > -1) {
             this.comprehensiveWorkflowMapping[workflow_id].versions[
               versionIndex2
-            ].icon_url = null;
+            ] = version;
           }
         }
         return version;
       });
     },
+    deleteWorkflowExtension(
+      workflow_id: string,
+      version_id: string,
+    ): Promise<void> {
+      return WorkflowVersionService.workflowVersionDeleteWorkflowVersionParameterExtension(
+        workflow_id,
+        version_id,
+      ).then(() => {
+        // Update version in workflowMapping
+        if (this.workflowMapping[workflow_id] == undefined) {
+          this.fetchWorkflow(workflow_id, false);
+        } else {
+          const versionIndex1 = this.workflowMapping[
+            workflow_id
+          ].versions.findIndex(
+            (version) => version.workflow_version_id == version_id,
+          );
+          if (versionIndex1 > -1) {
+            this.workflowMapping[workflow_id].versions[
+              versionIndex1
+            ].parameter_extension = undefined;
+          }
+        }
+
+        // Update version in comprehensiveWorkflowMapping
+        if (this.comprehensiveWorkflowMapping[workflow_id] == undefined) {
+          this.fetchWorkflow(workflow_id, true);
+        } else {
+          const versionIndex2 = this.comprehensiveWorkflowMapping[
+            workflow_id
+          ].versions.findIndex(
+            (version) => version.workflow_version_id == version_id,
+          );
+          if (versionIndex2 > -1) {
+            this.comprehensiveWorkflowMapping[workflow_id].versions[
+              versionIndex2
+            ].parameter_extension = undefined;
+          }
+        }
+      });
+    },
   },
 });
diff --git a/src/views/workflows/CreateParameterTranslationView.vue b/src/views/workflows/CreateParameterTranslationView.vue
index 7e5ee61..a2db38a 100644
--- a/src/views/workflows/CreateParameterTranslationView.vue
+++ b/src/views/workflows/CreateParameterTranslationView.vue
@@ -12,6 +12,7 @@ import { useResourceStore } from "@/stores/resources";
 import ParameterInput from "@/components/parameter-schema/form-mode/ParameterInput.vue";
 import BootstrapToast from "@/components/BootstrapToast.vue";
 import { Toast } from "bootstrap";
+import DeleteModal from "@/components/modals/DeleteModal.vue";
 
 // Props
 // =============================================================================
@@ -24,6 +25,7 @@ const props = defineProps<{
 // =============================================================================
 const parameterExtensionForm = ref<HTMLFormElement | null>(null);
 let successToast: Toast | null = null;
+let deleteToast: Toast | null = null;
 
 // Repositories
 // =============================================================================
@@ -35,6 +37,7 @@ const resourceRepository = useResourceStore();
 // =============================================================================
 const parameterState = reactive<{
   loading: boolean;
+  makingRequest: boolean;
   extension: ParameterExtension_Input;
   resourceParametersDefault: Set<string>;
   resourceParametersMapping: Set<string>;
@@ -42,6 +45,7 @@ const parameterState = reactive<{
   formValidated: boolean;
 }>({
   loading: true,
+  makingRequest: false,
   extension: {},
   resourceParametersDefault: new Set(),
   resourceParametersMapping: new Set(),
@@ -102,7 +106,11 @@ function updateParameterPools(newVal?: object) {
   if (newVal) {
     const parameters = extractParameterList(newVal);
     parameterPools.defaults = parameters.slice();
-    parameterPools.mapping = parameters.slice();
+    parameterPools.mapping = parameters.filter(
+      (param) =>
+        parameterSchema.value?.[param]?.["type"] !== "boolean" &&
+        !parameterSchema.value?.[param]?.["enum"],
+    );
   }
   if (
     workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension
@@ -137,6 +145,7 @@ function submitForm() {
   }
   parameterState.formValidated = true;
   if (parameterExtensionForm.value?.checkValidity()) {
+    parameterState.makingRequest = true;
     workflowRepository
       .updateWorkflowExtension(
         props.workflowId,
@@ -145,6 +154,9 @@ function submitForm() {
       )
       .then(() => {
         successToast?.show();
+      })
+      .finally(() => {
+        parameterState.makingRequest = false;
       });
   }
 }
@@ -222,6 +234,17 @@ function makeResourceParameterDefault(param: string) {
   };
 }
 
+function makeResourceParameterMapping(param: string, val: string) {
+  if (parameterState.extension.mapping?.[param]?.[val]) {
+    parameterState.formValidated = false;
+    parameterState.resourceParametersMapping.add(param);
+    parameterState.extension.mapping[param][val] = {
+      resource_id: "",
+      resource_version_id: "",
+    };
+  }
+}
+
 function deleteDefaultParameter(param: string) {
   if (
     !workflowRepository.documentationFiles[
@@ -284,14 +307,47 @@ function deleteMappingParameter(param: string) {
   }
 }
 
+function deleteParameterExtension() {
+  parameterState.makingRequest = true;
+  workflowRepository
+    .deleteWorkflowExtension(props.workflowId, props.versionId)
+    .then(() => {
+      parameterState.extension = {};
+      updateParameterPools(
+        workflowRepository.documentationFiles[props.versionId]
+          ?.parameter_schema,
+      );
+      updateResourceParameters(
+        workflowRepository.documentationFiles[props.versionId]?.clowm_info,
+      );
+      deleteToast?.show();
+    })
+    .finally(() => {
+      parameterState.makingRequest = false;
+    });
+}
+
 // Lifecycle Events
 // =============================================================================
 onMounted(() => {
-  successToast = new Toast("#save-parameter-extension-success");
+  successToast = new Toast("#save-parameter-extension-success-toast");
+  deleteToast = new Toast("#delete-parameter-extension-success-toast");
   workflowRepository.fetchWorkflow(props.workflowId, true, () => {
     parameterState.extension =
       workflowRepository.ownVersionMapping[props.versionId]
         ?.parameter_extension ?? {};
+    for (const param of Object.keys(parameterState.extension?.mapping ?? {})) {
+      for (const paramOption of Object.keys(
+        parameterState.extension?.mapping?.[param] ?? {},
+      )) {
+        if (
+          typeof parameterState.extension?.mapping?.[param]?.[paramOption] ===
+          "object"
+        ) {
+          parameterState.resourceParametersMapping.add(param);
+        }
+      }
+    }
     workflowRepository
       .fetchWorkflowDocumentation(
         props.workflowId,
@@ -318,13 +374,13 @@ onMounted(() => {
         );
       });
   });
-  resourceRepository.fetchPublicResources();
+
 });
 </script>
 
 <template>
   <bootstrap-toast
-    toast-id="save-parameter-extension-success"
+    toast-id="save-parameter-extension-success-toast"
     color-class="success"
   >
     <template #default>Successfully saved Parameter Extension</template>
@@ -351,13 +407,43 @@ onMounted(() => {
       </div>
     </template>
   </bootstrap-toast>
-  <div class="row border-bottom mb-4">
-    <h2 class="mb-2">
+  <bootstrap-toast
+    toast-id="delete-parameter-extension-success-toast"
+    color-class="success"
+  >
+    Successfully deleted Parameter Extension
+  </bootstrap-toast>
+  <delete-modal
+    v-if="
+      workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension
+    "
+    modal-id="delete-parameter-extension-modal"
+    :object-name-delete="`parameter extension of ${nameRepository.getName(props.workflowId)}@${nameRepository.getName(props.versionId)}`"
+    @confirm-delete="deleteParameterExtension"
+  />
+  <div class="d-flex justify-content-between border-bottom mb-4 pb-2">
+    <h2 class="w-fit">
       Add parameter metadata to
       {{ nameRepository.getName(props.workflowId) }}@{{
         nameRepository.getName(props.versionId)
       }}
     </h2>
+    <div
+      v-if="
+        workflowRepository.ownVersionMapping[props.versionId]
+          ?.parameter_extension
+      "
+    >
+      <button
+        type="button"
+        class="btn btn-danger"
+        data-bs-toggle="modal"
+        data-bs-target="#delete-parameter-extension-modal"
+        :disabled="parameterState.loading || parameterState.makingRequest"
+      >
+        Delete
+      </button>
+    </div>
   </div>
   <div v-if="parameterState.loading" class="d-flex justify-content-center">
     <div class="spinner-border" role="status">
@@ -446,7 +532,7 @@ onMounted(() => {
       class="d-flex flex-wrap overflow-y-auto p-1 border rounded-top border-dashed"
       style="max-height: 30vh"
     >
-      <b class="ms-1 w-100">Workflow parameters:</b>
+      <b class="ms-1 w-100">Eligible workflow parameters:</b>
       <template v-if="parameterPools.mapping.length > 0">
         <div
           class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container"
@@ -479,25 +565,27 @@ onMounted(() => {
             Remove
           </button>
         </div>
-        <div class="d-flex mb-4">
-          <button
-            type="button"
-            class="btn btn-primary me-2"
-            :disabled="
-              parameterState.mappingParameterValues[param]?.length === 0
-            "
-            @click="
-              addMappingParameterValue(
-                param,
-                parameterState.mappingParameterValues[param],
-              )
-            "
-          >
-            Add
-          </button>
+        <div class="d-flex mb-5">
+          <div class="me-2">
+            <button
+              type="button"
+              class="btn btn-primary"
+              :disabled="
+                parameterState.mappingParameterValues[param]?.length === 0
+              "
+              @click="
+                addMappingParameterValue(
+                  param,
+                  parameterState.mappingParameterValues[param],
+                )
+              "
+            >
+              Add Option
+            </button>
+          </div>
           <input
             type="text"
-            class="form-control"
+            class="form-control flex-fill w-fit"
             v-model="parameterState.mappingParameterValues[param]"
           />
         </div>
@@ -505,15 +593,21 @@ onMounted(() => {
           <div
             v-for="key in Object.keys(parameterState.extension.mapping[param])"
             :key="key"
-            class="mb-2"
+            class="mb-5 position-relative"
           >
-            <div class="p-0 d-flex justify-content-between mb-n1">
-              <code
-                class="p-1 rounded-top border-bottom-0 border border-secondary-subtle"
-                >{{ key }}</code
+            <code
+              class="p-1 position-absolute top-0 start-0 pt-0 rounded-top border-bottom-0 border border-secondary-subtle bla"
+              >{{ key }}</code
+            >
+            <div class="position-absolute top-0 end-0 bla">
+              <span
+                v-if="!parameterState.resourceParametersMapping.has(param)"
+                class="p-1 me-2 rounded-top border-bottom-0 border pseudo-primary-btn border-primary-subtle cursor-pointer"
+                @click="makeResourceParameterMapping(param, key)"
+                >Resource</span
               >
               <span
-                class="px-1 pb-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer"
+                class="p-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer"
                 @click="deleteMappingParameterValue(param, key)"
                 >Remove</span
               >
@@ -527,6 +621,9 @@ onMounted(() => {
                 size-modifier="sm"
                 border="secondary-subtle"
                 v-model="parameterState.extension.mapping[param][key]"
+                :resource-parameter="
+                  parameterState.resourceParametersMapping.has(param)
+                "
               />
             </div>
           </div>
@@ -541,6 +638,7 @@ onMounted(() => {
       form="parameter-extension-form"
       :disabled="
         parameterState.loading ||
+        parameterState.makingRequest ||
         Object.keys(parameterState.extension).length === 0
       "
     >
@@ -565,7 +663,18 @@ onMounted(() => {
   border-color: var(--bs-danger);
 }
 
-.mb-n1 {
-  margin-bottom: -0.25rem !important;
+.pseudo-primary-btn {
+  color: var(--bs-primary);
+  background-color: var(--bs-white);
+}
+
+.pseudo-primary-btn:hover {
+  color: var(--bs-white);
+  background-color: var(--bs-primary);
+  border-color: var(--bs-primary);
+}
+
+.bla {
+  transform: translateY(-90%) !important;
 }
 </style>
-- 
GitLab