From e6d95764bc1c06f2cccdcd6eaacd3935d9345373 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Fri, 9 Feb 2024 16:01:48 +0100
Subject: [PATCH] Add help page to create clowm info for existing workflows

#95
---
 package-lock.json                             | 250 ++++-----
 package.json                                  |   2 +
 src/App.vue                                   |   2 +-
 src/components/CopyToClipboardIcon.vue        |   1 +
 src/components/DraggableLists.vue             | 104 ++++
 .../ParameterSchemaFormComponent.vue          |   6 +-
 .../form-mode/ParameterBooleanInput.vue       |  15 +-
 .../form-mode/ParameterEnumInput.vue          |  26 +-
 .../form-mode/ParameterGroupForm.vue          |  49 +-
 .../form-mode/ParameterNumberInput.vue        |  20 +-
 .../form-mode/ParameterStringInput.vue        |  39 +-
 .../workflows/WorkflowDocumentationTabs.vue   |   2 +-
 .../workflows/WorkflowWithVersionsCard.vue    |  12 +
 .../workflows/modals/ParameterModal.vue       |   9 +-
 src/router/workflowRoutes.ts                  |  12 +
 src/utils/DownloadJson.ts                     |   6 +
 src/views/admin/AdminResourcesView.vue        |   2 +-
 src/views/admin/AdminUsersView.vue            |   2 +-
 src/views/object-storage/BucketsView.vue      |   5 +-
 src/views/object-storage/S3KeysView.vue       |   4 +-
 src/views/resources/ListResourcesView.vue     |   2 +-
 src/views/resources/MyResourcesView.vue       |   2 +-
 src/views/resources/ReviewResourceView.vue    |   2 +-
 src/views/workflows/CreateClowmInfoView.vue   | 485 ++++++++++++++++++
 .../workflows/ListWorkflowExecutionsView.vue  |   2 +-
 src/views/workflows/ListWorkflowsView.vue     |  14 +-
 src/views/workflows/MyWorkflowsView.vue       |   2 +-
 src/views/workflows/ReviewWorkflowsView.vue   |   2 +-
 28 files changed, 817 insertions(+), 262 deletions(-)
 create mode 100644 src/components/DraggableLists.vue
 create mode 100644 src/utils/DownloadJson.ts
 create mode 100644 src/views/workflows/CreateClowmInfoView.vue

diff --git a/package-lock.json b/package-lock.json
index fd24c5c..8927f4f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
         "pinia": "~2.1.0",
         "semver": "~7.5.0",
         "showdown": "~2.1.0",
+        "sortablejs": "^1.15.2",
         "vue": "~3.4.0",
         "vue-router": "~4.2.0",
         "vue3-cookies": "~1.0.0"
@@ -39,6 +40,7 @@
         "@types/node": "^18.19.5",
         "@types/semver": "~7.5.1",
         "@types/showdown": "~2.0.1",
+        "@types/sortablejs": "^1.15.7",
         "@vitejs/plugin-vue": "~5.0.0",
         "@vue/eslint-config-prettier": "~8.0.0",
         "@vue/eslint-config-typescript": "~11.0.3",
@@ -211,16 +213,16 @@
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
     "node_modules/@aws-sdk/client-s3": {
-      "version": "3.507.0",
-      "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.507.0.tgz",
-      "integrity": "sha512-rRLiC5Ly3e7kZVNoRsG6JhZ8Yat5uEnDeShdWNdHchyTO88AaEnHaeyiVG9ecmKI8jYl6NbWSHB8xL0l9KIr/w==",
+      "version": "3.509.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.509.0.tgz",
+      "integrity": "sha512-yeZJ1892Lj8S2zE0HerVt/ZJWaxemoEV3tzn5XDjExK6666cUajSwfmX036T51pEBwjqsTPz0ZJB1rlV7VFTIA==",
       "dependencies": {
         "@aws-crypto/sha1-browser": "3.0.0",
         "@aws-crypto/sha256-browser": "3.0.0",
         "@aws-crypto/sha256-js": "3.0.0",
         "@aws-sdk/client-sts": "3.507.0",
         "@aws-sdk/core": "3.496.0",
-        "@aws-sdk/credential-provider-node": "3.507.0",
+        "@aws-sdk/credential-provider-node": "3.509.0",
         "@aws-sdk/middleware-bucket-endpoint": "3.502.0",
         "@aws-sdk/middleware-expect-continue": "3.502.0",
         "@aws-sdk/middleware-flexible-checksums": "3.502.0",
@@ -500,9 +502,9 @@
       }
     },
     "node_modules/@aws-sdk/credential-provider-node": {
-      "version": "3.507.0",
-      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.507.0.tgz",
-      "integrity": "sha512-tkQnmOLkRBXfMLgDYHzogrqTNdtl0Im0ipzJb2IV5hfM5NoTfCf795e9A9isgwjSP/g/YEU0xQWxa4lq8LRtuA==",
+      "version": "3.509.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.509.0.tgz",
+      "integrity": "sha512-uXT8wIq1k+m0mS/pC9U1cUTIjUB7/4PgxyiYsTxYPIULtWnQXltAlcPU3QzKTJMP60sqftRYZ2jFDLAVsipQxw==",
       "dependencies": {
         "@aws-sdk/credential-provider-env": "3.502.0",
         "@aws-sdk/credential-provider-http": "3.503.1",
@@ -569,9 +571,9 @@
       }
     },
     "node_modules/@aws-sdk/lib-storage": {
-      "version": "3.507.0",
-      "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.507.0.tgz",
-      "integrity": "sha512-cwCBGw6W71d0PrFSRizBd7WdyzsZnRWVftnOvNkyjIE5vFifdFbpdVaFidMVUZmvUQvS3Vgkx1LeXPT98EBWUQ==",
+      "version": "3.509.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.509.0.tgz",
+      "integrity": "sha512-VivrFLMmLJkf27PNY+LDO0KqH3BCeH7j5zK3l7xQgJWmPiLcBvk2UXuANZKrP7V82+/kI/AXFaXJuycedEj8yA==",
       "dependencies": {
         "@smithy/abort-controller": "^2.1.1",
         "@smithy/middleware-endpoint": "^2.4.1",
@@ -772,9 +774,9 @@
       }
     },
     "node_modules/@aws-sdk/s3-request-presigner": {
-      "version": "3.507.0",
-      "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.507.0.tgz",
-      "integrity": "sha512-A3EGvXMeOvnG+qtAsmlcQyLP7+PlCePS+PsVqLm3Pz3C16avOTxTqOZIkYCqBBX2fnASr2qUr0d3cezBfsU7PQ==",
+      "version": "3.509.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.509.0.tgz",
+      "integrity": "sha512-6EwvUtoObMZ2s7cx3zDJBgcfvqXQ8ABoDNXcSm3Y8/hdhJq8ovICwanTSWkx6ylFw+TmPt0Qo57U4SeCQd0qYA==",
       "dependencies": {
         "@aws-sdk/signature-v4-multi-region": "3.502.0",
         "@aws-sdk/types": "3.502.0",
@@ -1772,9 +1774,9 @@
       }
     },
     "node_modules/@smithy/core": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz",
-      "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.2.tgz",
+      "integrity": "sha512-tYDmTp0f2TZVE18jAOH1PnmkngLQ+dOGUlMd1u67s87ieueNeyqhja6z/Z4MxhybEiXKOWFOmGjfTZWFxljwJw==",
       "dependencies": {
         "@smithy/middleware-endpoint": "^2.4.1",
         "@smithy/middleware-retry": "^2.1.1",
@@ -2245,9 +2247,9 @@
       }
     },
     "node_modules/@smithy/util-defaults-mode-node": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.1.1.tgz",
-      "integrity": "sha512-tYVrc+w+jSBfBd267KDnvSGOh4NMz+wVH7v4CClDbkdPfnjvImBZsOURncT5jsFwR9KCuDyPoSZq4Pa6+eCUrA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.0.tgz",
+      "integrity": "sha512-iFJp/N4EtkanFpBUtSrrIbtOIBf69KNuve03ic1afhJ9/korDxdM0c6cCH4Ehj/smI9pDCfVv+bqT3xZjF2WaA==",
       "dependencies": {
         "@smithy/config-resolver": "^2.1.1",
         "@smithy/credential-provider-imds": "^2.2.1",
@@ -2416,9 +2418,9 @@
       }
     },
     "node_modules/@types/node": {
-      "version": "18.19.14",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz",
-      "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==",
+      "version": "18.19.15",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
+      "integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
       "dev": true,
       "dependencies": {
         "undici-types": "~5.26.4"
@@ -2436,6 +2438,12 @@
       "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==",
       "dev": true
     },
+    "node_modules/@types/sortablejs": {
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.7.tgz",
+      "integrity": "sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==",
+      "dev": true
+    },
     "node_modules/@types/trusted-types": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -2672,12 +2680,12 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
-      "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.18.tgz",
+      "integrity": "sha512-F7YK8lMK0iv6b9/Gdk15A67wM0KKZvxDxed0RR60C1z9tIJTKta+urs4j0RTN5XqHISzI3etN3mX0uHhjmoqjQ==",
       "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/shared": "3.4.15",
+        "@babel/parser": "^7.23.9",
+        "@vue/shared": "3.4.18",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.0.2"
@@ -2689,26 +2697,26 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
-      "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.18.tgz",
+      "integrity": "sha512-24Eb8lcMfInefvQ6YlEVS18w5Q66f4+uXWVA+yb7praKbyjHRNuKVWGuinfSSjM0ZIiPi++QWukhkgznBaqpEA==",
       "dependencies": {
-        "@vue/compiler-core": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-core": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
-      "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
-      "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/compiler-core": "3.4.15",
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.18.tgz",
+      "integrity": "sha512-rG5tqtnzwrVpMqAQ7FHtvHaV70G6LLfJIWLYZB/jZ9m/hrnZmIQh+H3ewnC5onwe/ibljm9+ZupxeElzqCkTAw==",
+      "dependencies": {
+        "@babel/parser": "^7.23.9",
+        "@vue/compiler-core": "3.4.18",
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/compiler-ssr": "3.4.18",
+        "@vue/shared": "3.4.18",
         "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.5",
+        "magic-string": "^0.30.6",
         "postcss": "^8.4.33",
         "source-map-js": "^1.0.2"
       }
@@ -2730,12 +2738,12 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
-      "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.18.tgz",
+      "integrity": "sha512-hSlv20oUhPxo2UYUacHgGaxtqP0tvFo6ixxxD6JlXIkwzwoZ9eKK6PFQN4hNK/R13JlNyldwWt/fqGBKgWJ6nQ==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/devtools-api": {
@@ -2831,48 +2839,48 @@
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
-      "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.18.tgz",
+      "integrity": "sha512-7uda2/I0jpLiRygprDo5Jxs2HJkOVXcOMlyVlY54yRLxoycBpwGJRwJT9EdGB4adnoqJDXVT2BilUAYwI7qvmg==",
       "dependencies": {
-        "@vue/shared": "3.4.15"
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
-      "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.18.tgz",
+      "integrity": "sha512-7mU9diCa+4e+8/wZ7Udw5pwTH10A11sZ1nldmHOUKJnzCwvZxfJqAtw31mIf4T5H2FsLCSBQT3xgioA9vIjyDQ==",
       "dependencies": {
-        "@vue/reactivity": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/reactivity": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
-      "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.18.tgz",
+      "integrity": "sha512-2y1Mkzcw1niSfG7z3Qx+2ir9Gb4hdTkZe5p/I8x1aTIKQE0vY0tPAEUPhZm5tx6183gG3D/KwHG728UR0sIufA==",
       "dependencies": {
-        "@vue/runtime-core": "3.4.15",
-        "@vue/shared": "3.4.15",
+        "@vue/runtime-core": "3.4.18",
+        "@vue/shared": "3.4.18",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
-      "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.18.tgz",
+      "integrity": "sha512-YJd1wa7mzUN3NRqLEsrwEYWyO+PUBSROIGlCc3J/cvn7Zu6CxhNLgXa8Z4zZ5ja5/nviYO79J1InoPeXgwBTZA==",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-ssr": "3.4.18",
+        "@vue/shared": "3.4.18"
       },
       "peerDependencies": {
-        "vue": "3.4.15"
+        "vue": "3.4.18"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
-      "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.18.tgz",
+      "integrity": "sha512-CxouGFxxaW5r1WbrSmWwck3No58rApXgRSBxrqgnY1K+jk20F6DrXJkHdH9n4HVT+/B6G2CAn213Uq3npWiy8Q=="
     },
     "node_modules/@vue/tsconfig": {
       "version": "0.5.1",
@@ -3207,16 +3215,10 @@
       }
     },
     "node_modules/chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
       "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
@@ -3229,6 +3231,9 @@
       "engines": {
         "node": ">= 8.10.0"
       },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
       "optionalDependencies": {
         "fsevents": "~2.3.2"
       }
@@ -4157,13 +4162,14 @@
       }
     },
     "node_modules/get-symbol-description": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.1.tgz",
-      "integrity": "sha512-KmuibvwbWaM4BHcBRYwJfZ1JxyJeBwB8ct9YYu67SvYdbEIlcQ2e56dHxfbobqW38GXo8/zDFqJeGtHiVbWyQw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.5",
-        "es-errors": "^1.3.0"
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5507,9 +5513,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.34",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz",
-      "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==",
+      "version": "8.4.35",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
+      "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
       "funding": [
         {
           "type": "opencollective",
@@ -5866,13 +5872,13 @@
       ]
     },
     "node_modules/safe-regex-test": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
-      "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "get-intrinsic": "^1.2.2",
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
         "is-regex": "^1.1.4"
       },
       "engines": {
@@ -5914,14 +5920,15 @@
       }
     },
     "node_modules/set-function-length": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
-      "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.1",
+        "define-data-property": "^1.1.2",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.2",
+        "get-intrinsic": "^1.2.3",
         "gopd": "^1.0.1",
         "has-property-descriptors": "^1.0.1"
       },
@@ -5997,14 +6004,18 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
+      "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -6019,6 +6030,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/sortablejs": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz",
+      "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -6301,14 +6317,14 @@
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
-      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
+      "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "is-typed-array": "^1.1.10"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -6914,15 +6930,15 @@
       }
     },
     "node_modules/vue": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
-      "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
-      "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-sfc": "3.4.15",
-        "@vue/runtime-dom": "3.4.15",
-        "@vue/server-renderer": "3.4.15",
-        "@vue/shared": "3.4.15"
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.18.tgz",
+      "integrity": "sha512-0zLRYamFRe0wF4q2L3O24KQzLyLpL64ye1RUToOgOxuWZsb/FhaNRdGmeozdtVYLz6tl94OXLaK7/WQIrVCw1A==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/compiler-sfc": "3.4.18",
+        "@vue/runtime-dom": "3.4.18",
+        "@vue/server-renderer": "3.4.18",
+        "@vue/shared": "3.4.18"
       },
       "peerDependencies": {
         "typescript": "*"
diff --git a/package.json b/package.json
index ef34c45..0d6de8d 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "pinia": "~2.1.0",
     "semver": "~7.5.0",
     "showdown": "~2.1.0",
+    "sortablejs": "^1.15.2",
     "vue": "~3.4.0",
     "vue-router": "~4.2.0",
     "vue3-cookies": "~1.0.0"
@@ -46,6 +47,7 @@
     "@types/node": "^18.19.5",
     "@types/semver": "~7.5.1",
     "@types/showdown": "~2.0.1",
+    "@types/sortablejs": "^1.15.7",
     "@vitejs/plugin-vue": "~5.0.0",
     "@vue/eslint-config-prettier": "~8.0.0",
     "@vue/eslint-config-typescript": "~11.0.3",
diff --git a/src/App.vue b/src/App.vue
index 1ae0d2e..4497959 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -87,7 +87,7 @@ onBeforeMount(() => {
 
 <template>
   <NavbarTop />
-  <div class="container-xxl mt-4 flex-grow-1">
+  <div class="container-xxl mt-4 flex-grow-1 py-2">
     <div
       id="global-toast-container"
       class="toast-container position-fixed top-toast end-0 p-3"
diff --git a/src/components/CopyToClipboardIcon.vue b/src/components/CopyToClipboardIcon.vue
index b0d34b4..6e85078 100644
--- a/src/components/CopyToClipboardIcon.vue
+++ b/src/components/CopyToClipboardIcon.vue
@@ -47,6 +47,7 @@ onMounted(() => {
   </bootstrap-toast>
   <button v-if="props.button" @click="copyToClipboard" class="btn btn-primary">
     Copy to Clipboard
+    <font-awesome-icon icon="fa-solid fa-clipboard" class="ms-1" />
   </button>
   <span
     v-else
diff --git a/src/components/DraggableLists.vue b/src/components/DraggableLists.vue
new file mode 100644
index 0000000..bb72d1d
--- /dev/null
+++ b/src/components/DraggableLists.vue
@@ -0,0 +1,104 @@
+<script setup lang="ts">
+import { onMounted, ref } from "vue";
+import Sortable from "sortablejs";
+
+const leftList = defineModel<string[]>("leftList", { required: true });
+const rightList = defineModel<string[]>("rightList", { required: true });
+
+const leftListElement = ref<HTMLUListElement | undefined>(undefined);
+const rightListElement = ref<HTMLUListElement | undefined>(undefined);
+
+onMounted(() => {
+  if (leftListElement.value && rightListElement.value) {
+    new Sortable(leftListElement.value, {
+      group: "shared",
+      animation: 150,
+      sort: false, // To disable sorting: set sort to false
+      onRemove: (evt) => {
+        leftList.value.splice(evt.oldIndex ?? 0, 1);
+      },
+      onAdd: (evt) => {
+        leftList.value.splice(
+          evt.newIndex ?? 0,
+          0,
+          (evt.item as HTMLLIElement).innerText,
+        );
+      },
+    });
+
+    new Sortable(rightListElement.value, {
+      group: "shared",
+      animation: 150,
+      sort: false, // To disable sorting: set sort to false
+      onRemove: (evt) => {
+        rightList.value.splice(evt.oldIndex ?? 0, 1);
+      },
+      onAdd: (evt) => {
+        rightList.value.splice(
+          evt.newIndex ?? 0,
+          0,
+          (evt.item as HTMLLIElement).innerText,
+        );
+      },
+    });
+  }
+});
+</script>
+
+<template>
+  <div class="row">
+    <div class="col-6 d-flex flex-column justify-content-start">
+      <h5><slot name="leftHeader" /></h5>
+      <ul
+        id="items"
+        class="list-group flex-fill border border-dashed p-1 overflow-y-scroll"
+        ref="leftListElement"
+        style="max-height: 40vh"
+      >
+        <li
+          v-for="(left, index) in leftList"
+          class="list-group-item"
+          :key="left"
+          @click="
+            rightList.push(left);
+            leftList.splice(index, 1);
+          "
+        >
+          {{ left }}
+        </li>
+      </ul>
+    </div>
+    <div class="col-6 d-flex flex-column justify-content-start">
+      <h5><slot name="rightHeader" /></h5>
+      <ul
+        id="items"
+        class="list-group flex-fill border border-dashed p-1 overflow-y-scroll"
+        ref="rightListElement"
+        style="max-height: 40vh"
+      >
+        <li
+          v-for="(right, index) in rightList"
+          class="list-group-item"
+          :key="right"
+          @click="
+            leftList.push(right);
+            rightList.splice(index, 1);
+          "
+        >
+          {{ right }}
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+li:hover {
+  cursor: grab;
+  background: var(--bs-secondary-bg-subtle);
+}
+
+.border-dashed {
+  border-style: dashed !important;
+}
+</style>
diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
index 4a93a52..ed64353 100644
--- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue
+++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
@@ -183,6 +183,7 @@ function startWorkflow() {
   errorToast?.hide();
   formState.validated = true;
   formState.errorType = undefined;
+  console.log(formState.formInput);
   if (launchForm.value?.checkValidity()) {
     const schemaValid = validateSchema(formState.formInput);
 
@@ -232,7 +233,8 @@ function scroll(selectedAnchor: string) {
 // =============================================================================
 onMounted(() => {
   if (props.schema) updateSchema(props.schema);
-  if (props.clowmInfo?.exampleParameters) Tooltip.getOrCreateInstance("#exampleDataButton");
+  if (props.clowmInfo?.exampleParameters)
+    Tooltip.getOrCreateInstance("#exampleDataButton");
   bucketRepository.fetchBuckets();
   bucketRepository.fetchOwnPermissions();
   keyRepository.fetchS3Keys();
@@ -271,7 +273,7 @@ onMounted(() => {
         })
     "
   />
-  <div class="row mb-5 align-items-start">
+  <div class="row align-items-start">
     <form
       v-if="props.schema"
       class="col-9"
diff --git a/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue
index a30bd4e..54e519c 100644
--- a/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue
+++ b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue
@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { computed } from "vue";
 
+const model = defineModel<boolean | undefined>({ required: true });
+
 const props = defineProps({
   parameter: {
     type: Object,
@@ -14,9 +16,6 @@ const props = defineProps({
     type: String,
     required: true,
   },
-  modelValue: {
-    type: Boolean,
-  },
   helpId: {
     type: String,
   },
@@ -26,10 +25,6 @@ const randomIDSuffix = Math.random().toString(16).substring(2, 8);
 
 const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]);
 const iconPresent = computed<boolean>(() => props.parameter["fa_icon"]);
-
-const emit = defineEmits<{
-  (e: "update:modelValue", value: boolean): void;
-}>();
 </script>
 
 <template>
@@ -47,8 +42,7 @@ const emit = defineEmits<{
         :name="'inlineRadioOptions' + randomIDSuffix"
         :id="'trueOption' + randomIDSuffix"
         :value="true"
-        :checked="props.modelValue"
-        @input="emit('update:modelValue', true)"
+        v-model="model"
       />
     </div>
     <div class="form-check form-check-inline">
@@ -58,8 +52,7 @@ const emit = defineEmits<{
         :name="'inlineRadioOptions' + randomIDSuffix"
         :id="'falseOption' + randomIDSuffix"
         :value="false"
-        @input="emit('update:modelValue', false)"
-        :checked="!props.modelValue"
+        v-model="model"
       />
       <label class="form-check-label" :for="'falseOption' + randomIDSuffix"
         >False</label
diff --git a/src/components/parameter-schema/form-mode/ParameterEnumInput.vue b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue
index 03ec622..a92092c 100644
--- a/src/components/parameter-schema/form-mode/ParameterEnumInput.vue
+++ b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue
@@ -1,5 +1,7 @@
 <script setup lang="ts">
-import { computed, ref } from "vue";
+import { computed } from "vue";
+
+const model = defineModel<string | undefined>({ required: true });
 
 const props = defineProps({
   parameter: {
@@ -14,41 +16,23 @@ const props = defineProps({
     type: String,
     required: true,
   },
-  modelValue: {
-    type: String,
-  },
   helpId: {
     type: String,
   },
 });
 
 const possibleValues = computed<string[]>(() => props.parameter["enum"]);
-
-const enumSelection = ref<HTMLSelectElement | undefined>(undefined);
-
-const emit = defineEmits<{
-  (e: "update:modelValue", value: string | undefined): void;
-}>();
-
-function updateValue() {
-  emit("update:modelValue", enumSelection.value?.value);
-}
 </script>
 
 <template>
   <select
     ref="enumSelection"
-    :value="props.modelValue"
-    @input="updateValue"
+    v-model="model"
     class="form-select border border-secondary"
     :required="required"
     :aria-describedby="props.helpId"
   >
-    <option
-      v-for="val in possibleValues"
-      :key="val"
-      :selected="props.modelValue === val"
-    >
+    <option v-for="val in possibleValues" :key="val">
       {{ val }}
     </option>
   </select>
diff --git a/src/components/parameter-schema/form-mode/ParameterGroupForm.vue b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue
index be65227..836ff27 100644
--- a/src/components/parameter-schema/form-mode/ParameterGroupForm.vue
+++ b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue
@@ -1,11 +1,14 @@
 <script setup lang="ts">
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
-import { computed, type PropType, watch } from "vue";
+import { computed, type PropType } from "vue";
 import ParameterNumberInput from "@/components/parameter-schema/form-mode/ParameterNumberInput.vue";
 import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
 import ParameterBooleanInput from "@/components/parameter-schema/form-mode/ParameterBooleanInput.vue";
 import ParameterEnumInput from "@/components/parameter-schema/form-mode/ParameterEnumInput.vue";
 import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
+import type { WorkflowParameters } from "@/types/WorkflowParameters";
+
+const model = defineModel<WorkflowParameters>({ required: true });
 
 const props = defineProps({
   parameterGroup: {
@@ -19,10 +22,6 @@ const props = defineProps({
     type: String,
     required: true,
   },
-  modelValue: {
-    type: Object,
-    required: true,
-  },
   showHidden: {
     type: Boolean,
     default: false,
@@ -52,14 +51,6 @@ const parameters = computed<Record<string, never>>(
   () => props.parameterGroup["properties"],
 );
 
-const formInput = computed(() => props.modelValue);
-const emit = defineEmits<{
-  (
-    e: "update:modelValue",
-    value: Record<string, number | string | boolean | undefined>,
-  ): void;
-}>();
-
 function parameterRequired(
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   parameterGroup: Record<string, any>,
@@ -68,20 +59,10 @@ function parameterRequired(
   return (
     parameterGroup["required"]?.includes(parameterName) || // parameter is required
     parameterGroup["dependentRequired"]?.[parameterName] // parameter is required when another parameter is set
-      ?.map((param: string) => formInput.value[param])
+      ?.map((param: string) => model.value[param])
       ?.reduce((acc: boolean, val: string) => acc || val, false)
   );
 }
-
-watch(
-  formInput,
-  (newVal) => {
-    emit("update:modelValue", newVal);
-  },
-  {
-    deep: true,
-  },
-);
 </script>
 
 <template>
@@ -129,41 +110,29 @@ watch(
               :parameter="parameter"
               :help-id="parameterName + '-help'"
               :required="parameterRequired(parameterGroup, parameterName)"
-              :model-value="formInput[parameterName]"
-              @update:model-value="
-                (newValue) => (formInput[parameterName] = newValue)
-              "
+              v-model="model[parameterName]"
             />
             <parameter-boolean-input
               v-else-if="parameter['type'] === 'boolean'"
               :parameter-name="parameterName"
               :parameter="parameter"
               :help-id="parameterName + '-help'"
-              :model-value="formInput[parameterName]"
-              @update:model-value="
-                (newValue) => (formInput[parameterName] = newValue)
-              "
+              v-model="model[parameterName]"
             />
             <template v-else-if="parameter['type'] === 'string'">
               <parameter-enum-input
                 v-if="parameter['enum']"
                 :parameter-name="parameterName"
                 :parameter="parameter"
-                :model-value="formInput[parameterName]"
                 :required="parameterRequired(parameterGroup, parameterName)"
-                @update:model-value="
-                  (newValue) => (formInput[parameterName] = newValue)
-                "
+                v-model="model[parameterName]"
               />
               <parameter-string-input
                 v-else
                 :parameter-name="parameterName"
                 :parameter="parameter"
-                :model-value="formInput[parameterName]"
                 :required="parameterRequired(parameterGroup, parameterName)"
-                @update:model-value="
-                  (newValue) => (formInput[parameterName] = newValue)
-                "
+                v-model="model[parameterName]"
                 :remove-advanced="!showOptional"
                 :clowm-resource="resourceParameters?.includes(parameterName)"
               />
diff --git a/src/components/parameter-schema/form-mode/ParameterNumberInput.vue b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue
index 55963a2..0cc7ca6 100644
--- a/src/components/parameter-schema/form-mode/ParameterNumberInput.vue
+++ b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue
@@ -1,5 +1,6 @@
 <script setup lang="ts">
-import { ref } from "vue";
+const model = defineModel<number | undefined>({ required: true });
+
 const props = defineProps({
   parameter: {
     type: Object,
@@ -13,37 +14,22 @@ const props = defineProps({
     type: String,
     required: true,
   },
-  modelValue: {
-    type: Number,
-  },
   helpId: {
     type: String,
   },
 });
-
-const emit = defineEmits<{
-  (e: "update:modelValue", value: number | undefined): void;
-}>();
-
-const numberInput = ref<HTMLInputElement | undefined>(undefined);
-
-function updateValue() {
-  emit("update:modelValue", Number(numberInput.value?.value));
-}
 </script>
 
 <template>
   <input
     class="form-control border border-secondary"
     type="number"
-    ref="numberInput"
     :max="props.parameter['maximum']"
     :min="props.parameter['minimum']"
     :step="props.parameter['type'] === 'integer' ? 1 : 0.0001"
-    :value="props.modelValue"
+    v-model="model"
     :required="props.required"
     :aria-describedby="props.helpId"
-    @input="updateValue"
   />
 </template>
 
diff --git a/src/components/parameter-schema/form-mode/ParameterStringInput.vue b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
index a41cc04..fe6d014 100644
--- a/src/components/parameter-schema/form-mode/ParameterStringInput.vue
+++ b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
@@ -10,6 +10,8 @@ const s3objectRepository = useS3ObjectStore();
 const resourceRepository = useResourceStore();
 const randomIDSuffix = Math.random().toString(16).substring(2, 8);
 
+const model = defineModel<string | undefined>({ required: true });
+
 const props = defineProps({
   parameter: {
     type: Object,
@@ -23,9 +25,6 @@ const props = defineProps({
     type: String,
     required: true,
   },
-  modelValue: {
-    type: String,
-  },
   helpId: {
     type: String,
   },
@@ -39,10 +38,6 @@ const props = defineProps({
   },
 });
 
-const emit = defineEmits<{
-  (e: "update:modelValue", value?: string): void;
-}>();
-
 const s3Path = reactive<{
   bucket: string;
   key?: string;
@@ -61,10 +56,8 @@ const selectedResource = reactive<{
 
 const formState = reactive<{
   advancedInput: boolean;
-  stringVal?: string;
 }>({
   advancedInput: false,
-  stringVal: undefined,
 });
 
 const stringInput = ref<HTMLInputElement | undefined>(undefined);
@@ -131,22 +124,17 @@ watch(
   },
 );
 
-watch(() => formState.stringVal, updateValue);
 watch(selectedResource, () => {
   if (clowmResource.value && !formState.advancedInput) {
     updateStringFromResource();
   }
 });
 
-watch(
-  () => props.modelValue,
-  (newVal) => {
-    if (formState.stringVal != newVal) {
-      formState.stringVal = newVal;
-      formState.advancedInput = true;
-    }
-  },
-);
+watch(model, (newVal, oldVal) => {
+  if (newVal && newVal !== oldVal) {
+    formState.advancedInput = true;
+  }
+});
 
 watch(
   () => formState.advancedInput,
@@ -161,18 +149,14 @@ watch(
   },
 );
 
-function updateValue() {
-  emit("update:modelValue", formState.stringVal);
-}
-
 function updateStringFromS3() {
-  formState.stringVal = !s3Path.bucket
+  model.value = !s3Path.bucket
     ? undefined
     : `s3://${s3Path.bucket}${s3Path.key ? "/" + s3Path.key : ""}`;
 }
 
 function updateStringFromResource() {
-  formState.stringVal =
+  model.value =
     resourceRepository.resourceMapping[selectedResource.resourceId]?.versions[
       selectedResource.resourceVersionIndex
     ]?.cluster_path ?? undefined;
@@ -188,8 +172,7 @@ function updateKeysInBucket(bucketName?: string) {
 }
 
 onMounted(() => {
-  formState.stringVal = props.modelValue;
-  if (formState.stringVal) {
+  if (model.value) {
     formState.advancedInput = true;
   }
 });
@@ -265,7 +248,7 @@ onMounted(() => {
       class="form-control border border-secondary"
       :class="{ 'rounded-end': props.removeAdvanced && !helpTextPresent }"
       type="text"
-      v-model="formState.stringVal"
+      v-model="model"
       :required="props.required"
       :aria-describedby="props.helpId"
       :pattern="pattern"
diff --git a/src/components/workflows/WorkflowDocumentationTabs.vue b/src/components/workflows/WorkflowDocumentationTabs.vue
index 7ce8afc..7ebecac 100644
--- a/src/components/workflows/WorkflowDocumentationTabs.vue
+++ b/src/components/workflows/WorkflowDocumentationTabs.vue
@@ -102,7 +102,7 @@ const activeTab = computed<string>(
       ></span>
     </p>
   </div>
-  <div v-else class="px-2 pt-3 border border-top-0 mb-2">
+  <div v-else class="px-2 pt-3 border border-top-0">
     <div v-if="activeTab === 'description'">
       <markdown-renderer
         v-if="props.descriptionMarkdown"
diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue
index defb55e..3def45d 100644
--- a/src/components/workflows/WorkflowWithVersionsCard.vue
+++ b/src/components/workflows/WorkflowWithVersionsCard.vue
@@ -126,6 +126,18 @@ onMounted(() => {
           <span>{{ props.workflow.name }}</span>
         </div>
         <div>
+          <router-link
+            :to="{
+              name: 'workflows-clowminfo',
+              query: {
+                workflow_id: workflow.workflow_id,
+                workflow_version_id: sortedVersions(props.workflow.versions)[0]
+                  ?.workflow_version_id,
+              },
+            }"
+            class="btn btn-primary"
+            >Clowm
+          </router-link>
           <button
             type="button"
             class="btn btn-outline-info me-2"
diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue
index 31c33ca..5ae588a 100644
--- a/src/components/workflows/modals/ParameterModal.vue
+++ b/src/components/workflows/modals/ParameterModal.vue
@@ -9,6 +9,7 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import type { WorkflowExecutionOut, WorkflowVersion } from "@/client/workflow";
 import { useNameStore } from "@/stores/names";
 import { useWorkflowStore } from "@/stores/workflows";
+import { createDownloadUrl } from "@/utils/DownloadJson";
 import type { WorkflowParameters } from "@/types/WorkflowParameters";
 
 const nameRepository = useNameStore();
@@ -80,10 +81,10 @@ const parameterDownloadUrl = computed<string | undefined>(() => {
   if (parameters.value == undefined) {
     return undefined;
   }
-  const blob = new Blob([JSON.stringify(parameters.value, undefined, 2)], {
-    type: "application/json",
-  });
-  return URL.createObjectURL(blob);
+  return createDownloadUrl(
+    JSON.stringify(parameters.value, null, 2),
+    "application/json",
+  );
 });
 
 const workflowName = computed<string>(() => {
diff --git a/src/router/workflowRoutes.ts b/src/router/workflowRoutes.ts
index f4d076c..4441fcb 100644
--- a/src/router/workflowRoutes.ts
+++ b/src/router/workflowRoutes.ts
@@ -20,6 +20,18 @@ export const workflowRoutes: RouteRecordRaw[] = [
       requiresDeveloperRole: true,
     },
   },
+  {
+    path: "developer/workflows/clowminfo",
+    name: "workflows-clowminfo",
+    component: () => import("../views/workflows/CreateClowmInfoView.vue"),
+    props: (route) => ({
+      workflow_version_id: route.query.workflow_version_id ?? undefined,
+      workflow_id: route.query.workflow_id ?? undefined,
+    }),
+    meta: {
+      requiresDeveloperRole: true,
+    },
+  },
   {
     path: "reviewer/workflows",
     name: "workflows-reviewer",
diff --git a/src/utils/DownloadJson.ts b/src/utils/DownloadJson.ts
new file mode 100644
index 0000000..9ad1f78
--- /dev/null
+++ b/src/utils/DownloadJson.ts
@@ -0,0 +1,6 @@
+export function createDownloadUrl(content: string, mimeType?: string): string {
+  const blob = new Blob([content], {
+    type: mimeType,
+  });
+  return URL.createObjectURL(blob);
+}
diff --git a/src/views/admin/AdminResourcesView.vue b/src/views/admin/AdminResourcesView.vue
index 095336b..9528652 100644
--- a/src/views/admin/AdminResourcesView.vue
+++ b/src/views/admin/AdminResourcesView.vue
@@ -147,7 +147,7 @@ function resetForm() {
     :resource="resourceState.inspectResource"
   />
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center"
+    class="row border-bottom mb-4 justify-content-between align-items-center"
   >
     <h2>Manage Resources</h2>
   </div>
diff --git a/src/views/admin/AdminUsersView.vue b/src/views/admin/AdminUsersView.vue
index a1f3e14..58eeccc 100644
--- a/src/views/admin/AdminUsersView.vue
+++ b/src/views/admin/AdminUsersView.vue
@@ -43,7 +43,7 @@ function searchUsers() {
 
 <template>
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center"
+    class="row border-bottom mb-4 justify-content-between align-items-center"
   >
     <h2>Manage Users</h2>
   </div>
diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue
index b19d7d8..1797014 100644
--- a/src/views/object-storage/BucketsView.vue
+++ b/src/views/object-storage/BucketsView.vue
@@ -92,11 +92,10 @@ onMounted(() => {
     modalID="create-bucket-modal"
     v-if="!authStore.foreignUser"
   />
-  <div class="row m-2 border-bottom">
-    <div class="col-12"></div>
+  <div class="row border-bottom">
     <h2 class="mb-2">My Data Buckets</h2>
   </div>
-  <div class="row m-2 mt-4">
+  <div class="row mt-4">
     <div class="col-3">
       <div class="d-flex justify-content-between">
         <button
diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue
index 8fce1fe..018415a 100644
--- a/src/views/object-storage/S3KeysView.vue
+++ b/src/views/object-storage/S3KeysView.vue
@@ -80,11 +80,11 @@ onMounted(() => {
   <bootstrap-toast toast-id="successKeyToast">
     Successfully deleted S3 Key {{ keyState.deletedKey }}
   </bootstrap-toast>
-  <div class="row m-2 border-bottom mt-4">
+  <div class="row border-bottom mt-4">
     <div class="col-12"></div>
     <h2 class="mb-2">S3 Keys</h2>
   </div>
-  <div class="row m-2 mt-4">
+  <div class="row mt-4">
     <div class="col-4">
       <div class="d-flex justify-content-between mb-4">
         <button
diff --git a/src/views/resources/ListResourcesView.vue b/src/views/resources/ListResourcesView.vue
index b615344..5787adc 100644
--- a/src/views/resources/ListResourcesView.vue
+++ b/src/views/resources/ListResourcesView.vue
@@ -53,7 +53,7 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="row m-2 border-bottom mb-4">
+  <div class="row border-bottom mb-4">
     <h2 class="mb-2">Available Resources</h2>
   </div>
   <div class="d-flex m-2 mb-3 align-items-center justify-content-start">
diff --git a/src/views/resources/MyResourcesView.vue b/src/views/resources/MyResourcesView.vue
index e9a3d23..5b2b365 100644
--- a/src/views/resources/MyResourcesView.vue
+++ b/src/views/resources/MyResourcesView.vue
@@ -62,7 +62,7 @@ onMounted(() => {
     modal-id="updateResourceModal"
   />
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2"
+    class="row border-bottom mb-4 justify-content-between align-items-center pb-2"
   >
     <h2 class="w-fit">My Resources</h2>
     <div class="w-fit">
diff --git a/src/views/resources/ReviewResourceView.vue b/src/views/resources/ReviewResourceView.vue
index 78f6785..e682c79 100644
--- a/src/views/resources/ReviewResourceView.vue
+++ b/src/views/resources/ReviewResourceView.vue
@@ -75,7 +75,7 @@ onMounted(() => {
     :resource="resourceState.inspectResource"
   />
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center"
+    class="row border-bottom mb-4 justify-content-between align-items-center"
   >
     <h2 class="w-fit">Resource Requests</h2>
     <span
diff --git a/src/views/workflows/CreateClowmInfoView.vue b/src/views/workflows/CreateClowmInfoView.vue
new file mode 100644
index 0000000..0c2c089
--- /dev/null
+++ b/src/views/workflows/CreateClowmInfoView.vue
@@ -0,0 +1,485 @@
+<script setup lang="ts">
+import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue";
+import {
+  computed,
+  onMounted,
+  onBeforeMount,
+  reactive,
+  watch,
+  onUnmounted,
+} from "vue";
+import { createDownloadUrl } from "@/utils/DownloadJson";
+import type { ClowmInfo } from "@/types/ClowmInfo";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import { useWorkflowStore } from "@/stores/workflows";
+import { DocumentationEnum } from "@/client/workflow";
+import DraggableLists from "@/components/DraggableLists.vue";
+
+const props = defineProps<{
+  workflow_id?: string;
+  workflow_version_id?: string;
+}>();
+
+const workflowRepository = useWorkflowStore();
+
+const infoState = reactive<ClowmInfo>({
+  inputParameters: [],
+  outputParameters: [],
+  exampleParameters: undefined,
+  dois: undefined,
+  resourceParameters: undefined,
+});
+
+const parameterPools = reactive<{
+  input: string[];
+  output: string[];
+  resources: string[];
+  examples: string[];
+}>({
+  examples: [],
+  input: [],
+  output: [],
+  resources: [],
+});
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const parameterSchema = computed<Record<string, Record<string, any>>>(() => {
+  const schema =
+    workflowRepository.documentationFiles[props.workflow_version_id ?? ""]
+      ?.parameter_schema;
+  const a = schema?.["properties"] ?? {};
+  for (const group in schema?.["definitions"] ?? {}) {
+    for (const param in schema?.["definitions"]?.[group]?.["properties"] ??
+      {}) {
+      a[param] = schema["definitions"][group]["properties"][param];
+    }
+  }
+  return a;
+});
+
+function getParameterType(param: string): string | undefined {
+  return parameterSchema.value[param]?.["type"];
+}
+
+const infoStateString = computed<string>(() =>
+  JSON.stringify(infoState, null, 2),
+);
+const downloadUrl = computed<string>(() =>
+  createDownloadUrl(infoStateString.value, "application/json"),
+);
+
+watch(
+  () => workflowRepository.documentationFiles[props.workflow_version_id ?? ""],
+  (newVal) => {
+    updateParameterPools(newVal);
+  },
+  {
+    deep: true,
+  },
+);
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function updateParameterPools(newVal?: Record<DocumentationEnum, any>) {
+  if (newVal?.parameter_schema) {
+    const parameters = extractParameterList(newVal.parameter_schema);
+    parameterPools.input = parameters.slice();
+    parameterPools.output = parameters.slice();
+    parameterPools.resources = parameters.slice();
+    parameterPools.examples = parameters.slice();
+    infoState.inputParameters = [];
+    infoState.outputParameters = [];
+    infoState.exampleParameters = undefined;
+    infoState.resourceParameters = undefined;
+  }
+  if (newVal?.clowm_info) {
+    Object.assign(infoState, JSON.parse(JSON.stringify(newVal.clowm_info)));
+    parameterPools.input = parameterPools.input.filter(
+      (param) => !infoState.inputParameters.includes(param),
+    );
+    parameterPools.resources = parameterPools.resources?.filter(
+      (param) => !infoState.resourceParameters?.includes(param),
+    );
+    parameterPools.output = parameterPools.output.filter(
+      (param) => !infoState.outputParameters.includes(param),
+    );
+    parameterPools.examples = parameterPools.examples?.filter(
+      (param) =>
+        !Object.keys(infoState.exampleParameters ?? {}).includes(param),
+    );
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function extractParameterList(schema: Record<string, any>): string[] {
+  const groupedParameters = Object.keys(schema["definitions"] ?? {}).reduce(
+    (acc: string[], val) => [
+      ...acc,
+      ...Object.keys(schema["definitions"][val]["properties"]),
+    ],
+    [],
+  );
+  const singleParameters = Object.keys(schema["properties"] ?? {});
+  return [...groupedParameters, ...singleParameters];
+}
+
+function addDoi() {
+  if (infoState.dois == undefined) {
+    infoState.dois = [""];
+  } else {
+    infoState.dois.push("");
+  }
+}
+
+function removeDoi(index: number) {
+  if (infoState.dois?.length === 1) {
+    infoState.dois = undefined;
+  }
+  infoState.dois?.splice(index, 1);
+}
+
+onBeforeMount(() => {
+  if (props.workflow_id && props.workflow_version_id) {
+    workflowRepository.fetchWorkflowDocumentation(
+      props.workflow_id,
+      props.workflow_version_id,
+      DocumentationEnum.PARAMETER_SCHEMA,
+    );
+    workflowRepository.fetchWorkflowDocumentation(
+      props.workflow_id,
+      props.workflow_version_id,
+      DocumentationEnum.CLOWM_INFO,
+    );
+  }
+});
+
+onMounted(() => {
+  if (props.workflow_id && props.workflow_version_id) {
+    updateParameterPools(
+      workflowRepository.documentationFiles[props.workflow_version_id],
+    );
+  }
+});
+
+onUnmounted(() => {
+  console.log("Unmounted");
+});
+</script>
+
+<template>
+  <div class="row border-bottom mb-4">
+    <h2 class="mb-2">Create <code>clowm_info.json</code></h2>
+  </div>
+  <div class="accordion mb-2" id="clowmInfoAccordion">
+    <div class="accordion-item">
+      <h2 class="accordion-header">
+        <button
+          class="accordion-button"
+          type="button"
+          data-bs-toggle="collapse"
+          data-bs-target="#clowmInfoAccordion-input"
+          aria-expanded="true"
+          aria-controls="clowmInfoAccordion-input"
+        >
+          Input Parameters
+        </button>
+      </h2>
+      <div
+        id="clowmInfoAccordion-input"
+        class="accordion-collapse collapse show"
+        data-bs-parent="#clowmInfoAccordion"
+      >
+        <div class="accordion-body">
+          <draggable-lists
+            :left-list="parameterPools.input"
+            :right-list="infoState.inputParameters"
+          >
+            <template #leftHeader>Workflow Parameters</template>
+            <template #rightHeader>Input Parameters</template>
+          </draggable-lists>
+        </div>
+      </div>
+    </div>
+    <div class="accordion-item">
+      <h2 class="accordion-header">
+        <button
+          class="accordion-button collapsed"
+          type="button"
+          data-bs-toggle="collapse"
+          data-bs-target="#clowmInfoAccordion-output"
+          aria-expanded="false"
+          aria-controls="clowmInfoAccordion-output"
+        >
+          Output Parameters
+        </button>
+      </h2>
+      <div
+        id="clowmInfoAccordion-output"
+        class="accordion-collapse collapse"
+        data-bs-parent="#clowmInfoAccordion"
+      >
+        <div class="accordion-body">
+          <draggable-lists
+            :left-list="parameterPools.output"
+            :right-list="infoState.outputParameters"
+          >
+            <template #leftHeader>Workflow Parameters</template>
+            <template #rightHeader>Output Parameters</template>
+          </draggable-lists>
+        </div>
+      </div>
+    </div>
+    <div class="accordion-item">
+      <h2 class="accordion-header">
+        <button
+          class="accordion-button collapsed"
+          type="button"
+          data-bs-toggle="collapse"
+          data-bs-target="#clowmInfoAccordion-resource"
+          aria-expanded="false"
+          aria-controls="clowmInfoAccordion-resource"
+        >
+          Resource Parameters
+        </button>
+      </h2>
+      <div
+        id="clowmInfoAccordion-resource"
+        class="accordion-collapse collapse"
+        data-bs-parent="#clowmInfoAccordion"
+      >
+        <div class="accordion-body">
+          <button
+            v-if="infoState.resourceParameters == undefined"
+            type="button"
+            class="btn btn-primary"
+            @click="infoState.resourceParameters = []"
+          >
+            This workflow needs resources
+          </button>
+          <draggable-lists
+            :left-list="parameterPools.resources"
+            :right-list="infoState.resourceParameters"
+            v-else
+          >
+            <template #leftHeader>Workflow Parameters</template>
+            <template #rightHeader>Resources Parameters</template>
+          </draggable-lists>
+        </div>
+      </div>
+    </div>
+    <div class="accordion-item">
+      <h2 class="accordion-header">
+        <button
+          class="accordion-button collapsed"
+          type="button"
+          data-bs-toggle="collapse"
+          data-bs-target="#clowmInfoAccordion-example"
+          aria-expanded="false"
+          aria-controls="clowmInfoAccordion-example"
+        >
+          Example Parameters
+        </button>
+      </h2>
+      <div
+        id="clowmInfoAccordion-example"
+        class="accordion-collapse collapse"
+        data-bs-parent="#clowmInfoAccordion"
+      >
+        <div class="accordion-body">
+          <button
+            v-if="infoState.exampleParameters == undefined"
+            type="button"
+            class="btn btn-primary"
+            @click="infoState.exampleParameters = {}"
+          >
+            I have Examples
+          </button>
+          <template v-else>
+            <div
+              class="d-flex flex-wrap overflow-y-scroll p-1 border rounded border-dashed mb-2"
+              style="max-height: 30vh"
+            >
+              <div
+                class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container"
+                v-for="(param, index) in parameterPools.examples"
+                :key="param"
+                @click="
+                  parameterPools.examples.splice(index, 1);
+                  infoState.exampleParameters[param] = '';
+                "
+              >
+                {{ param }}
+              </div>
+            </div>
+            <table class="table table-bordered">
+              <thead>
+                <tr>
+                  <th scope="col"><b>Parameter</b></th>
+                  <th scope="col"><b>Value</b></th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr
+                  v-for="param in Object.keys(infoState.exampleParameters)"
+                  :key="param"
+                >
+                  <td style="width: 10%">{{ param }}</td>
+                  <td class="d-flex justify-content-between align-items-center">
+                    <input
+                      v-if="
+                        getParameterType(param) === 'number' ||
+                        getParameterType(param) === 'integer'
+                      "
+                      type="number"
+                      class="form-control form-control-sm flex-grow"
+                      v-model="infoState.exampleParameters[param]"
+                      :step="getParameterType(param) === 'integer' ? 1 : 0.0001"
+                      :min="parameterSchema[param]['minimum']"
+                      :max="parameterSchema[param]['maximum']"
+                    />
+                    <div
+                      v-else-if="getParameterType(param) === 'boolean'"
+                      class="flex-grow"
+                    >
+                      <div class="form-check form-check-inline">
+                        <label
+                          class="form-check-label"
+                          :for="'trueOption' + param.replace(/\./g, '')"
+                          >True</label
+                        >
+                        <input
+                          class="form-check-input"
+                          type="radio"
+                          :name="
+                            'inlineRadioOptions' + param.replace(/\./g, '')
+                          "
+                          :id="'trueOption' + param.replace(/\./g, '')"
+                          :value="true"
+                          v-model="infoState.exampleParameters[param]"
+                        />
+                      </div>
+                      <div class="form-check form-check-inline">
+                        <input
+                          class="form-check-input"
+                          type="radio"
+                          :name="
+                            'inlineRadioOptions' + param.replace(/\./g, '')
+                          "
+                          :id="'falseOption' + param.replace(/\./g, '')"
+                          :value="false"
+                          v-model="infoState.exampleParameters[param]"
+                        />
+                        <label
+                          class="form-check-label"
+                          :for="'falseOption' + param.replace(/\./g, '')"
+                          >False</label
+                        >
+                      </div>
+                    </div>
+                    <select
+                      v-else-if="parameterSchema[param]?.['enum']"
+                      class="form-select form-select-sm flex-grow"
+                      v-model="infoState.exampleParameters[param]"
+                    >
+                      <option
+                        v-for="option in parameterSchema[param]?.['enum']"
+                        :key="option"
+                        :value="option"
+                      >
+                        {{ option }}
+                      </option>
+                    </select>
+                    <input
+                      v-else
+                      type="text"
+                      class="form-control form-control-sm flex-grow"
+                      v-model="infoState.exampleParameters[param]"
+                      :pattern="parameterSchema[param]?.['pattern']"
+                    />
+                    <button
+                      type="button"
+                      class="btn btn-outline-danger btn-sm ms-2"
+                      @click="
+                        delete infoState.exampleParameters[param];
+                        parameterPools.examples.push(param);
+                      "
+                    >
+                      Remove
+                    </button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </template>
+        </div>
+      </div>
+    </div>
+    <div class="accordion-item">
+      <h2 class="accordion-header">
+        <button
+          class="accordion-button collapsed"
+          type="button"
+          data-bs-toggle="collapse"
+          data-bs-target="#clowmInfoAccordion-dois"
+          aria-expanded="false"
+          aria-controls="clowmInfoAccordion-dois"
+        >
+          DOIs
+        </button>
+      </h2>
+      <div
+        id="clowmInfoAccordion-dois"
+        class="accordion-collapse collapse"
+        data-bs-parent="#clowmInfoAccordion"
+      >
+        <div class="accordion-body">
+          <button type="button" class="btn btn-primary" @click="addDoi">
+            Add DOI
+          </button>
+          <div v-if="infoState.dois">
+            <div
+              class="d-flex my-2"
+              v-for="(doi, index) in infoState.dois"
+              :key="index"
+            >
+              <button
+                type="button"
+                class="btn btn-outline-danger btn-sm"
+                @click="removeDoi(index)"
+              >
+                Remove
+              </button>
+              <input
+                type="text"
+                class="form-control mx-2"
+                v-model="infoState.dois[index]"
+                maxlength="48"
+                style="max-width: 400px"
+              />
+              <div class="align-self-center">
+                Link:
+                <a
+                  :href="'https://doi.org/' + doi"
+                  target="_blank"
+                  class="align-self-center"
+                  >{{ doi }}</a
+                >
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <pre class="rounded w-100"><code>{{ infoState }}</code></pre>
+  <copy-to-clipboard-icon button :text="infoStateString" />
+  <a class="btn btn-primary ms-2" :href="downloadUrl" download="clowm_info.json"
+    >Download to file
+    <font-awesome-icon icon="fa-solid fa-download" class="ms-1" />
+  </a>
+</template>
+
+<style scoped>
+.parameter-container:hover {
+  background: var(--bs-secondary-bg-subtle);
+}
+</style>
diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue
index c3d87aa..5b96f03 100644
--- a/src/views/workflows/ListWorkflowExecutionsView.vue
+++ b/src/views/workflows/ListWorkflowExecutionsView.vue
@@ -165,7 +165,7 @@ onUnmounted(() => {
     :execution-id="executionsState.executionParameters"
   />
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center"
+    class="row border-bottom mb-4 justify-content-between align-items-center"
   >
     <h2 class="mb-2 w-fit">My Workflow Executions</h2>
     <div class="w-fit">
diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue
index 56ccb5a..4c54cf1 100644
--- a/src/views/workflows/ListWorkflowsView.vue
+++ b/src/views/workflows/ListWorkflowsView.vue
@@ -51,15 +51,15 @@ function filterWorkflowWithoutVersion(workflow: WorkflowOut): boolean {
 }
 
 const processedWorkflows = computed<WorkflowOut[]>(() => {
-  return [
-    ...workflowRepository.workflows.filter(
-      (workflow) =>
-        filterWorkflowByString(workflow) &&
-        filterWorkflowWithoutVersion(workflow),
-    ),
-  ].sort((a, b) =>
+  const temp = workflowRepository.workflows.filter(
+    (workflow) =>
+      filterWorkflowByString(workflow) &&
+      filterWorkflowWithoutVersion(workflow),
+  );
+  temp.sort((a, b) =>
     filterFunctionMapping[workflowsState.sortByAttribute](a, b) ? 1 : -1,
   );
+  return temp;
 });
 
 onMounted(() => {
diff --git a/src/views/workflows/MyWorkflowsView.vue b/src/views/workflows/MyWorkflowsView.vue
index 48d1c28..8b5b935 100644
--- a/src/views/workflows/MyWorkflowsView.vue
+++ b/src/views/workflows/MyWorkflowsView.vue
@@ -114,7 +114,7 @@ onMounted(() => {
     modal-i-d="updateWorkflowCredentialsModal"
   />
   <div
-    class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2"
+    class="row border-bottom mb-4 justify-content-between align-items-center pb-2"
   >
     <h2 class="w-fit">My Workflows</h2>
     <button
diff --git a/src/views/workflows/ReviewWorkflowsView.vue b/src/views/workflows/ReviewWorkflowsView.vue
index 8507574..ad15851 100644
--- a/src/views/workflows/ReviewWorkflowsView.vue
+++ b/src/views/workflows/ReviewWorkflowsView.vue
@@ -63,7 +63,7 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="row m-2 border-bottom mb-4">
+  <div class="row border-bottom mb-4">
     <h2 class="mb-2">Workflow Reviews</h2>
   </div>
   <div v-if="workflowsState.loading" class="text-center mt-5">
-- 
GitLab