From 59f7846627385a767a4035bbbca17ec885ac87f1 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 20 Aug 2024 15:25:21 +0200
Subject: [PATCH 001/131] generalize activity bar

---
 .../ActivityBar/ActivityBar.test.js           |  2 +-
 .../components/ActivityBar/ActivityBar.vue    | 57 ++++++++++++++-----
 .../ActivityBar/ActivitySettings.test.js      |  3 +-
 .../ActivityBar/ActivitySettings.vue          |  3 +-
 .../src/components/Panels/SettingsPanel.vue   |  8 ++-
 client/src/stores/activitySetup.ts            |  4 +-
 client/src/stores/activityStore.test.ts       |  6 +-
 client/src/stores/activityStore.ts            | 52 ++++++++++++++---
 8 files changed, 101 insertions(+), 34 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.test.js b/client/src/components/ActivityBar/ActivityBar.test.js
index 1fbab6c98cad..8d1b7fc8bfb4 100644
--- a/client/src/components/ActivityBar/ActivityBar.test.js
+++ b/client/src/components/ActivityBar/ActivityBar.test.js
@@ -45,7 +45,7 @@ describe("ActivityBar", () => {
 
     beforeEach(async () => {
         const pinia = createTestingPinia({ stubActions: false });
-        activityStore = useActivityStore();
+        activityStore = useActivityStore("default");
         eventStore = useEventStore();
         wrapper = shallowMount(mountTarget, {
             localVue,
diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index cde4d7b190ad..af8bacca559f 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -1,11 +1,10 @@
 <script setup lang="ts">
 import { storeToRefs } from "pinia";
-import { computed, type Ref, ref, watch } from "vue";
+import { computed, type Ref, ref } from "vue";
 import { useRoute } from "vue-router/composables";
 import draggable from "vuedraggable";
 
 import { useConfig } from "@/composables/config";
-import { useHashedUserId } from "@/composables/hashedUserId";
 import { convertDropData } from "@/stores/activitySetup";
 import { type Activity, useActivityStore } from "@/stores/activityStore";
 import { useEventStore } from "@/stores/eventStore";
@@ -24,6 +23,19 @@ import NotificationsPanel from "@/components/Panels/NotificationsPanel.vue";
 import SettingsPanel from "@/components/Panels/SettingsPanel.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
 
+const props = withDefaults(
+    defineProps<{
+        defaultActivities?: Activity[];
+        activityBarId?: string;
+        specialActivities?: Activity[];
+    }>(),
+    {
+        defaultActivities: undefined,
+        activityBarId: "default",
+        specialActivities: () => [],
+    }
+);
+
 // require user to long click before dragging
 const DRAG_DELAY = 50;
 
@@ -32,10 +44,8 @@ const { config, isConfigLoaded } = useConfig();
 const route = useRoute();
 const userStore = useUserStore();
 
-const { hashedUserId } = useHashedUserId();
-
 const eventStore = useEventStore();
-const activityStore = useActivityStore();
+const activityStore = useActivityStore(props.activityBarId);
 const { isAdmin, isAnonymous } = storeToRefs(userStore);
 
 const emit = defineEmits(["dragstart"]);
@@ -50,9 +60,6 @@ const dragItem: Ref<Activity | null> = ref(null);
 // drag state
 const isDragging = ref(false);
 
-// sync built-in activities with cached activities
-activityStore.sync();
-
 /**
  * Checks if the route of an activity is currently being visited and panels are collapsed
  */
@@ -132,12 +139,9 @@ function onToggleSidebar(toggle: string = "", to: string | null = null) {
     userStore.toggleSideBar(toggle);
 }
 
-watch(
-    () => hashedUserId.value,
-    () => {
-        activityStore.sync();
-    }
-);
+defineExpose({
+    isActiveSideBar,
+});
 </script>
 
 <template>
@@ -226,6 +230,28 @@ watch(
                     tooltip="Administer this Galaxy"
                     variant="danger"
                     @click="onToggleSidebar('admin')" />
+                <template v-for="activity in props.specialActivities">
+                    <ActivityItem
+                        v-if="activity.panel"
+                        :id="`activity-${activity.id}`"
+                        :key="activity.id"
+                        :icon="activity.icon"
+                        :is-active="panelActivityIsActive(activity)"
+                        :title="activity.title"
+                        :tooltip="activity.tooltip"
+                        :to="activity.to || ''"
+                        @click="onToggleSidebar(activity.id, activity.to)" />
+                    <ActivityItem
+                        v-else-if="activity.to"
+                        :id="`activity-${activity.id}`"
+                        :key="activity.id"
+                        :icon="activity.icon"
+                        :is-active="isActiveRoute(activity.to)"
+                        :title="activity.title"
+                        :tooltip="activity.tooltip"
+                        :to="activity.to"
+                        @click="onToggleSidebar()" />
+                </template>
             </b-nav>
         </div>
         <FlexPanel v-if="isSideBarOpen" side="left" :collapsible="false">
@@ -234,8 +260,9 @@ watch(
             <VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
             <MultiviewPanel v-else-if="isActiveSideBar('multiview')" />
             <NotificationsPanel v-else-if="isActiveSideBar('notifications')" />
-            <SettingsPanel v-else-if="isActiveSideBar('settings')" />
+            <SettingsPanel v-else-if="isActiveSideBar('settings')" :activity-bar-scope="props.activityBarId" />
             <AdminPanel v-else-if="isActiveSideBar('admin')" />
+            <slot name="side-panel"></slot>
         </FlexPanel>
     </div>
 </template>
diff --git a/client/src/components/ActivityBar/ActivitySettings.test.js b/client/src/components/ActivityBar/ActivitySettings.test.js
index 2fd33b00d02e..fc80db0b912e 100644
--- a/client/src/components/ActivityBar/ActivitySettings.test.js
+++ b/client/src/components/ActivityBar/ActivitySettings.test.js
@@ -40,13 +40,14 @@ describe("ActivitySettings", () => {
 
     beforeEach(async () => {
         const pinia = createTestingPinia({ stubActions: false });
-        activityStore = useActivityStore();
+        activityStore = useActivityStore("default");
         activityStore.sync();
         wrapper = mount(mountTarget, {
             localVue,
             pinia,
             props: {
                 query: "",
+                activityBarScope: "default",
             },
             stubs: {
                 icon: { template: "<div></div>" },
diff --git a/client/src/components/ActivityBar/ActivitySettings.vue b/client/src/components/ActivityBar/ActivitySettings.vue
index fe2ecce28dd5..946c78a19965 100644
--- a/client/src/components/ActivityBar/ActivitySettings.vue
+++ b/client/src/components/ActivityBar/ActivitySettings.vue
@@ -19,10 +19,11 @@ library.add({
 });
 
 const props = defineProps<{
+    activityBarScope: string;
     query: string;
 }>();
 
-const activityStore = useActivityStore();
+const activityStore = useActivityStore(props.activityBarScope);
 const { activities } = storeToRefs(activityStore);
 const activityAction = useActivityAction();
 
diff --git a/client/src/components/Panels/SettingsPanel.vue b/client/src/components/Panels/SettingsPanel.vue
index a30b7329b705..013911c8e9ce 100644
--- a/client/src/components/Panels/SettingsPanel.vue
+++ b/client/src/components/Panels/SettingsPanel.vue
@@ -10,7 +10,11 @@ import ActivitySettings from "@/components/ActivityBar/ActivitySettings.vue";
 import DelayedInput from "@/components/Common/DelayedInput.vue";
 import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
 
-const activityStore = useActivityStore();
+const props = defineProps<{
+    activityBarScope: string;
+}>();
+
+const activityStore = useActivityStore(props.activityBarScope);
 
 const confirmRestore = ref(false);
 const query = ref("");
@@ -37,7 +41,7 @@ function onQuery(newQuery: string) {
                 <FontAwesomeIcon :icon="faUndo" fixed-width />
             </BButton>
         </template>
-        <ActivitySettings :query="query" />
+        <ActivitySettings :query="query" :activity-bar-scope="props.activityBarScope" />
         <BModal
             v-model="confirmRestore"
             title="Restore Activity Bar Defaults"
diff --git a/client/src/stores/activitySetup.ts b/client/src/stores/activitySetup.ts
index 17551fc89e2b..80d3c0221161 100644
--- a/client/src/stores/activitySetup.ts
+++ b/client/src/stores/activitySetup.ts
@@ -4,7 +4,7 @@
 import { type Activity } from "@/stores/activityStore";
 import { type EventData } from "@/stores/eventStore";
 
-export const Activities = [
+export const defaultActivities = [
     {
         anonymous: false,
         description: "Displays currently running interactive tools (ITs), if these are enabled by the administrator.",
@@ -148,7 +148,7 @@ export const Activities = [
         to: "/libraries",
         visible: true,
     },
-];
+] as const;
 
 export function convertDropData(data: EventData): Activity | null {
     if (data.history_content_type === "dataset") {
diff --git a/client/src/stores/activityStore.test.ts b/client/src/stores/activityStore.test.ts
index e5fee99c70ea..b37e5546d1fe 100644
--- a/client/src/stores/activityStore.test.ts
+++ b/client/src/stores/activityStore.test.ts
@@ -56,14 +56,14 @@ describe("Activity Store", () => {
     });
 
     it("initialize store", () => {
-        const activityStore = useActivityStore();
+        const activityStore = useActivityStore("default");
         expect(activityStore.getAll().length).toBe(0);
         activityStore.sync();
         expect(activityStore.getAll().length).toBe(1);
     });
 
     it("add activity", () => {
-        const activityStore = useActivityStore();
+        const activityStore = useActivityStore("default");
         activityStore.sync();
         const initialActivities = activityStore.getAll();
         expect(initialActivities[0]?.visible).toBeTruthy();
@@ -81,7 +81,7 @@ describe("Activity Store", () => {
     });
 
     it("remove activity", () => {
-        const activityStore = useActivityStore();
+        const activityStore = useActivityStore("default");
         activityStore.sync();
         const initialActivities = activityStore.getAll();
         expect(initialActivities.length).toEqual(1);
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index 377a68e08964..d9dbce64ec69 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -1,13 +1,14 @@
 /**
  * Stores the Activity Bar state
  */
+import { watchImmediate } from "@vueuse/core";
+import { computed, type Ref, ref } from "vue";
 
-import { defineStore } from "pinia";
-import { type Ref } from "vue";
-
+import { useHashedUserId } from "@/composables/hashedUserId";
 import { useUserLocalStorage } from "@/composables/userLocalStorage";
 
-import { Activities } from "./activitySetup";
+import { defaultActivities } from "./activitySetup";
+import { defineScopedStore } from "./scopedStore";
 
 export interface Activity {
     // determine wether an anonymous user can access this activity
@@ -34,14 +35,36 @@ export interface Activity {
     visible: boolean;
 }
 
-export const useActivityStore = defineStore("activityStore", () => {
-    const activities: Ref<Array<Activity>> = useUserLocalStorage("activity-store-activities", []);
+export const useActivityStore = defineScopedStore("activityStore", (scope) => {
+    const activities: Ref<Array<Activity>> = useUserLocalStorage(`activity-store-activities-${scope}`, []);
+
+    const { hashedUserId } = useHashedUserId();
+
+    watchImmediate(
+        () => hashedUserId.value,
+        () => {
+            sync();
+        }
+    );
+
+    const customDefaultActivities = ref<Activity[] | null>(null);
+    const currentDefaultActivities = computed(() => customDefaultActivities.value ?? defaultActivities);
+
+    function overrideDefaultActivities(activities: Activity[]) {
+        customDefaultActivities.value = activities;
+        sync();
+    }
+
+    function resetDefaultActivities() {
+        customDefaultActivities.value = null;
+        sync();
+    }
 
     /**
      * Restores the default activity bar items
      */
     function restore() {
-        activities.value = Activities.slice();
+        activities.value = currentDefaultActivities.value.slice();
     }
 
     /**
@@ -52,12 +75,15 @@ export const useActivityStore = defineStore("activityStore", () => {
     function sync() {
         // create a map of built-in activities
         const activitiesMap: Record<string, Activity> = {};
-        Activities.forEach((a) => {
+
+        currentDefaultActivities.value.forEach((a) => {
             activitiesMap[a.id] = a;
         });
+
         // create an updated array of activities
         const newActivities: Array<Activity> = [];
         const foundActivity = new Set();
+
         activities.value.forEach((a: Activity) => {
             if (a.mutable) {
                 // existing custom activity
@@ -66,6 +92,7 @@ export const useActivityStore = defineStore("activityStore", () => {
                 // update existing built-in activity attributes
                 // skip legacy built-in activities
                 const sourceActivity = activitiesMap[a.id];
+
                 if (sourceActivity) {
                     foundActivity.add(a.id);
                     newActivities.push({
@@ -75,12 +102,14 @@ export const useActivityStore = defineStore("activityStore", () => {
                 }
             }
         });
+
         // add new built-in activities
-        Activities.forEach((a) => {
+        currentDefaultActivities.value.forEach((a) => {
             if (!foundActivity.has(a.id)) {
                 newActivities.push({ ...a });
             }
         });
+
         // update activities stored in local cache only if changes were applied
         if (JSON.stringify(activities.value) !== JSON.stringify(newActivities)) {
             activities.value = newActivities;
@@ -97,6 +126,7 @@ export const useActivityStore = defineStore("activityStore", () => {
 
     function remove(activityId: string) {
         const findIndex = activities.value.findIndex((a: Activity) => a.id === activityId);
+
         if (findIndex !== -1) {
             activities.value.splice(findIndex, 1);
         }
@@ -109,5 +139,9 @@ export const useActivityStore = defineStore("activityStore", () => {
         setAll,
         restore,
         sync,
+        customDefaultActivities,
+        currentDefaultActivities,
+        overrideDefaultActivities,
+        resetDefaultActivities,
     };
 });

From dfb99706bb969c71f3c3792327194dbd8009613c Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 20 Aug 2024 15:43:07 +0200
Subject: [PATCH 002/131] add activity bar to workflow editor

---
 .../components/ActivityBar/ActivityBar.vue    | 17 ++++++++-
 .../src/components/Workflow/Editor/Index.vue  | 38 +++++++++++++------
 .../Workflow/Editor/modules/activities.ts     | 31 +++++++++++++++
 3 files changed, 74 insertions(+), 12 deletions(-)
 create mode 100644 client/src/components/Workflow/Editor/modules/activities.ts

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index af8bacca559f..ab5de16f5431 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { watchImmediate } from "@vueuse/core";
 import { storeToRefs } from "pinia";
 import { computed, type Ref, ref } from "vue";
 import { useRoute } from "vue-router/composables";
@@ -28,11 +29,13 @@ const props = withDefaults(
         defaultActivities?: Activity[];
         activityBarId?: string;
         specialActivities?: Activity[];
+        showAdmin?: boolean;
     }>(),
     {
         defaultActivities: undefined,
         activityBarId: "default",
         specialActivities: () => [],
+        showAdmin: true,
     }
 );
 
@@ -46,6 +49,18 @@ const userStore = useUserStore();
 
 const eventStore = useEventStore();
 const activityStore = useActivityStore(props.activityBarId);
+
+watchImmediate(
+    () => props.defaultActivities,
+    (defaults) => {
+        if (defaults) {
+            activityStore.overrideDefaultActivities(defaults);
+        } else {
+            activityStore.resetDefaultActivities();
+        }
+    }
+);
+
 const { isAdmin, isAnonymous } = storeToRefs(userStore);
 
 const emit = defineEmits(["dragstart"]);
@@ -222,7 +237,7 @@ defineExpose({
                     tooltip="View additional activities"
                     @click="onToggleSidebar('settings')" />
                 <ActivityItem
-                    v-if="isAdmin"
+                    v-if="isAdmin && showAdmin"
                     id="activity-admin"
                     icon="user-cog"
                     :is-active="isActiveSideBar('admin')"
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 1943de38e215..a0919caf86ec 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -27,17 +27,25 @@
                 <b-form-textarea v-model="saveAsAnnotation" />
             </b-form-group>
         </b-modal>
-        <FlexPanel side="left">
-            <ToolPanel
-                workflow
-                :module-sections="moduleSections"
-                :data-managers="dataManagers"
-                :editor-workflows="workflows"
-                @onInsertTool="onInsertTool"
-                @onInsertModule="onInsertModule"
-                @onInsertWorkflow="onInsertWorkflow"
-                @onInsertWorkflowSteps="onInsertWorkflowSteps" />
-        </FlexPanel>
+        <ActivityBar
+            ref="activityBar"
+            :default-activities="workflowEditorActivities"
+            :special-activities="specialWorkflowActivities"
+            activity-bar-id="workflow-editor"
+            :show-admin="false">
+            <template v-slot:side-panel>
+                <ToolPanel
+                    v-if="activityBar.isActiveSideBar('workflow-editor-tools')"
+                    workflow
+                    :module-sections="moduleSections"
+                    :data-managers="dataManagers"
+                    :editor-workflows="workflows"
+                    @onInsertTool="onInsertTool"
+                    @onInsertModule="onInsertModule"
+                    @onInsertWorkflow="onInsertWorkflow"
+                    @onInsertWorkflowSteps="onInsertWorkflowSteps" />
+            </template>
+        </ActivityBar>
         <div id="center" class="workflow-center">
             <div class="editor-top-bar" unselectable="on">
                 <span>
@@ -212,6 +220,7 @@ import { Services } from "../services";
 import { InsertStepAction, useStepActions } from "./Actions/stepActions";
 import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
 import { defaultPosition } from "./composables/useDefaultStepPosition";
+import { specialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
 import { fromSimple } from "./modules/model";
 import { getModule, getVersions, loadWorkflow, saveWorkflow } from "./modules/services";
 import { getStateUpgradeMessages } from "./modules/utilities";
@@ -225,6 +234,7 @@ import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
 import SaveChangesModal from "./SaveChangesModal.vue";
 import StateUpgradeModal from "./StateUpgradeModal.vue";
 import WorkflowGraph from "./WorkflowGraph.vue";
+import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
 import FlexPanel from "@/components/Panels/FlexPanel.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
@@ -236,6 +246,7 @@ library.add(faArrowLeft, faArrowRight, faHistory);
 
 export default {
     components: {
+        ActivityBar,
         MarkdownEditor,
         FlexPanel,
         SaveChangesModal,
@@ -457,6 +468,8 @@ export default {
 
         const stepActions = useStepActions(stepStore, undoRedoStore, stateStore, connectionStore);
 
+        const activityBar = ref(null);
+
         return {
             id,
             name,
@@ -496,6 +509,7 @@ export default {
             initialLoading,
             stepActions,
             undoRedoStore,
+            activityBar,
         };
     },
     data() {
@@ -520,6 +534,8 @@ export default {
             debounceTimer: null,
             showSaveChangesModal: false,
             navUrl: "",
+            workflowEditorActivities,
+            specialWorkflowActivities,
         };
     },
     computed: {
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
new file mode 100644
index 000000000000..a413f79110d8
--- /dev/null
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -0,0 +1,31 @@
+export const workflowEditorActivities = [
+    {
+        anonymous: true,
+        description: "Displays the tool panel to search and access all available tools.",
+        icon: "wrench",
+        id: "workflow-editor-tools",
+        mutable: false,
+        optional: false,
+        panel: true,
+        title: "Tools",
+        to: null,
+        tooltip: "Search and run tools",
+        visible: true,
+    },
+] as const;
+
+export const specialWorkflowActivities = [
+    {
+        anonymous: true,
+        description: "Displays the tool panel to search and access all available tools.",
+        icon: "wrench",
+        id: "tools",
+        mutable: false,
+        optional: false,
+        panel: true,
+        title: "Tools",
+        to: null,
+        tooltip: "Search and run tools",
+        visible: true,
+    },
+] as const;

From 9777bde58aad3d95b63330fabbd02bca947549bd Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 21 Aug 2024 17:40:27 +0200
Subject: [PATCH 003/131] add activities to activity bar debounce sync to avoid
 staggered calls pass activity bar id where needed

---
 .../components/ActivityBar/ActivityBar.vue    | 12 +++---
 .../ActivityBar/ActivitySettings.vue          |  6 +--
 .../components/Panels/InvocationsPanel.vue    |  8 +++-
 .../src/components/Panels/SettingsPanel.vue   |  6 +--
 .../src/components/Workflow/Editor/Index.vue  |  8 +---
 .../Workflow/Editor/modules/activities.ts     |  6 ++-
 client/src/composables/useActivityAction.ts   |  9 ++---
 client/src/stores/activityStore.ts            | 40 ++++++++++++++++---
 client/src/stores/userStore.ts                |  8 ----
 9 files changed, 63 insertions(+), 40 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index ab5de16f5431..052ef9091491 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -86,10 +86,10 @@ function isActiveRoute(activityTo: string) {
  * Checks if a panel has been expanded
  */
 function isActiveSideBar(menuKey: string) {
-    return userStore.toggledSideBar === menuKey;
+    return activityStore.toggledSideBar === menuKey;
 }
 
-const isSideBarOpen = computed(() => userStore.toggledSideBar !== "");
+const isSideBarOpen = computed(() => activityStore.toggledSideBar !== "");
 
 /**
  * Checks if an activity that has a panel should have the `is-active` prop
@@ -151,7 +151,7 @@ function onToggleSidebar(toggle: string = "", to: string | null = null) {
     if (toggle && to && !(route.path === to) && isActiveSideBar(toggle)) {
         return;
     }
-    userStore.toggleSideBar(toggle);
+    activityStore.toggleSideBar(toggle);
 }
 
 defineExpose({
@@ -271,13 +271,13 @@ defineExpose({
         </div>
         <FlexPanel v-if="isSideBarOpen" side="left" :collapsible="false">
             <ToolPanel v-if="isActiveSideBar('tools')" />
-            <InvocationsPanel v-else-if="isActiveSideBar('invocation')" />
+            <InvocationsPanel v-else-if="isActiveSideBar('invocation')" :activity-bar-id="props.activityBarId" />
             <VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
             <MultiviewPanel v-else-if="isActiveSideBar('multiview')" />
             <NotificationsPanel v-else-if="isActiveSideBar('notifications')" />
-            <SettingsPanel v-else-if="isActiveSideBar('settings')" :activity-bar-scope="props.activityBarId" />
+            <SettingsPanel v-else-if="isActiveSideBar('settings')" :activity-bar-id="props.activityBarId" />
             <AdminPanel v-else-if="isActiveSideBar('admin')" />
-            <slot name="side-panel"></slot>
+            <slot name="side-panel" :is-active-side-bar="isActiveSideBar"></slot>
         </FlexPanel>
     </div>
 </template>
diff --git a/client/src/components/ActivityBar/ActivitySettings.vue b/client/src/components/ActivityBar/ActivitySettings.vue
index 946c78a19965..7f55a5fc59e9 100644
--- a/client/src/components/ActivityBar/ActivitySettings.vue
+++ b/client/src/components/ActivityBar/ActivitySettings.vue
@@ -19,13 +19,13 @@ library.add({
 });
 
 const props = defineProps<{
-    activityBarScope: string;
+    activityBarId: string;
     query: string;
 }>();
 
-const activityStore = useActivityStore(props.activityBarScope);
+const activityStore = useActivityStore(props.activityBarId);
 const { activities } = storeToRefs(activityStore);
-const activityAction = useActivityAction();
+const activityAction = useActivityAction(props.activityBarId);
 
 const filteredActivities = computed(() => {
     if (props.query?.length > 0) {
diff --git a/client/src/components/Panels/InvocationsPanel.vue b/client/src/components/Panels/InvocationsPanel.vue
index ba810258676e..2d404dde6637 100644
--- a/client/src/components/Panels/InvocationsPanel.vue
+++ b/client/src/components/Panels/InvocationsPanel.vue
@@ -3,12 +3,18 @@ import { BAlert } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { ref } from "vue";
 
+import { useActivityStore } from "@/stores/activityStore";
 import { useUserStore } from "@/stores/userStore";
 
 import InvocationScrollList from "../Workflow/Invocation/InvocationScrollList.vue";
 import ActivityPanel from "./ActivityPanel.vue";
 
-const { currentUser, toggledSideBar } = storeToRefs(useUserStore());
+const props = defineProps<{
+    activityBarId: string;
+}>();
+
+const { currentUser } = storeToRefs(useUserStore());
+const { toggledSideBar } = storeToRefs(useActivityStore(props.activityBarId));
 
 const shouldCollapse = ref(false);
 function collapseOnLeave() {
diff --git a/client/src/components/Panels/SettingsPanel.vue b/client/src/components/Panels/SettingsPanel.vue
index 013911c8e9ce..c9f073cf0bc8 100644
--- a/client/src/components/Panels/SettingsPanel.vue
+++ b/client/src/components/Panels/SettingsPanel.vue
@@ -11,10 +11,10 @@ import DelayedInput from "@/components/Common/DelayedInput.vue";
 import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
 
 const props = defineProps<{
-    activityBarScope: string;
+    activityBarId: string;
 }>();
 
-const activityStore = useActivityStore(props.activityBarScope);
+const activityStore = useActivityStore(props.activityBarId);
 
 const confirmRestore = ref(false);
 const query = ref("");
@@ -41,7 +41,7 @@ function onQuery(newQuery: string) {
                 <FontAwesomeIcon :icon="faUndo" fixed-width />
             </BButton>
         </template>
-        <ActivitySettings :query="query" :activity-bar-scope="props.activityBarScope" />
+        <ActivitySettings :query="query" :activity-bar-id="props.activityBarId" />
         <BModal
             v-model="confirmRestore"
             title="Restore Activity Bar Defaults"
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index a0919caf86ec..677e36ea2e02 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -28,14 +28,13 @@
             </b-form-group>
         </b-modal>
         <ActivityBar
-            ref="activityBar"
             :default-activities="workflowEditorActivities"
             :special-activities="specialWorkflowActivities"
             activity-bar-id="workflow-editor"
             :show-admin="false">
-            <template v-slot:side-panel>
+            <template v-slot:side-panel="{ isActiveSideBar }">
                 <ToolPanel
-                    v-if="activityBar.isActiveSideBar('workflow-editor-tools')"
+                    v-if="isActiveSideBar('workflow-editor-tools')"
                     workflow
                     :module-sections="moduleSections"
                     :data-managers="dataManagers"
@@ -468,8 +467,6 @@ export default {
 
         const stepActions = useStepActions(stepStore, undoRedoStore, stateStore, connectionStore);
 
-        const activityBar = ref(null);
-
         return {
             id,
             name,
@@ -509,7 +506,6 @@ export default {
             initialLoading,
             stepActions,
             undoRedoStore,
-            activityBar,
         };
     },
     data() {
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index a413f79110d8..ccfe7e12af24 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,3 +1,5 @@
+import type { Activity } from "@/stores/activityStore";
+
 export const workflowEditorActivities = [
     {
         anonymous: true,
@@ -12,7 +14,7 @@ export const workflowEditorActivities = [
         tooltip: "Search and run tools",
         visible: true,
     },
-] as const;
+] as const satisfies Readonly<Activity[]>;
 
 export const specialWorkflowActivities = [
     {
@@ -28,4 +30,4 @@ export const specialWorkflowActivities = [
         tooltip: "Search and run tools",
         visible: true,
     },
-] as const;
+] as const satisfies Readonly<Activity[]>;
diff --git a/client/src/composables/useActivityAction.ts b/client/src/composables/useActivityAction.ts
index 3dc1babc7f84..da9db5501851 100644
--- a/client/src/composables/useActivityAction.ts
+++ b/client/src/composables/useActivityAction.ts
@@ -1,14 +1,13 @@
 import { useRouter } from "vue-router/composables";
 
-import { type Activity } from "@/stores/activityStore";
-import { useUserStore } from "@/stores/userStore";
+import { type Activity, useActivityStore } from "@/stores/activityStore";
 
-export function useActivityAction() {
+export function useActivityAction(activityStoreId: string) {
     const router = useRouter();
-    const userStore = useUserStore();
+    const activityStore = useActivityStore(activityStoreId);
     const executeActivity = (activity: Activity) => {
         if (activity.panel) {
-            userStore.toggleSideBar(activity.id);
+            activityStore.toggleSideBar(activity.id);
         }
         if (activity.to) {
             router.push(activity.to);
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index d9dbce64ec69..77306911bbb1 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -1,7 +1,7 @@
 /**
  * Stores the Activity Bar state
  */
-import { watchImmediate } from "@vueuse/core";
+import { useDebounceFn, watchImmediate } from "@vueuse/core";
 import { computed, type Ref, ref } from "vue";
 
 import { useHashedUserId } from "@/composables/hashedUserId";
@@ -33,6 +33,8 @@ export interface Activity {
     tooltip: string;
     // indicate wether the activity should be visible by default
     visible: boolean;
+    // if activity should cause a click event
+    click?: true;
 }
 
 export const useActivityStore = defineScopedStore("activityStore", (scope) => {
@@ -40,6 +42,15 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
 
     const { hashedUserId } = useHashedUserId();
 
+    const customDefaultActivities = ref<Activity[] | null>(null);
+    const currentDefaultActivities = computed(() => customDefaultActivities.value ?? defaultActivities);
+
+    const toggledSideBar = useUserLocalStorage(`activity-store-current-side-bar-${scope}`, "tools");
+
+    function toggleSideBar(currentOpen = "") {
+        toggledSideBar.value = toggledSideBar.value === currentOpen ? "" : currentOpen;
+    }
+
     watchImmediate(
         () => hashedUserId.value,
         () => {
@@ -47,9 +58,6 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         }
     );
 
-    const customDefaultActivities = ref<Activity[] | null>(null);
-    const currentDefaultActivities = computed(() => customDefaultActivities.value ?? defaultActivities);
-
     function overrideDefaultActivities(activities: Activity[]) {
         customDefaultActivities.value = activities;
         sync();
@@ -72,7 +80,7 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
      * This helper function applies changes of the built-in activities,
      * to the user stored activities which are persisted in local cache.
      */
-    function sync() {
+    const sync = useDebounceFn(() => {
         // create a map of built-in activities
         const activitiesMap: Record<string, Activity> = {};
 
@@ -114,7 +122,25 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         if (JSON.stringify(activities.value) !== JSON.stringify(newActivities)) {
             activities.value = newActivities;
         }
-    }
+
+        // if toggled side-bar does not exist, choose the first option
+        if (toggledSideBar.value !== "") {
+            const allSideBars = activities.value.flatMap((activity) => {
+                if (activity.panel) {
+                    return [activity.id];
+                } else {
+                    return [];
+                }
+            });
+
+            const allSideBarsSet = new Set(allSideBars);
+            const firstSideBar = allSideBars[0];
+
+            if (firstSideBar && !allSideBarsSet.has(toggledSideBar.value)) {
+                toggledSideBar.value = firstSideBar;
+            }
+        }
+    }, 10);
 
     function getAll() {
         return activities.value;
@@ -133,6 +159,8 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
     }
 
     return {
+        toggledSideBar,
+        toggleSideBar,
         activities,
         getAll,
         remove,
diff --git a/client/src/stores/userStore.ts b/client/src/stores/userStore.ts
index 55a49d491209..a83daa94b5ff 100644
--- a/client/src/stores/userStore.ts
+++ b/client/src/stores/userStore.ts
@@ -27,8 +27,6 @@ export const useUserStore = defineStore("userStore", () => {
     const currentUser = ref<AnyUser>(null);
     const currentPreferences = ref<Preferences | null>(null);
 
-    // explicitly pass current User, because userStore might not exist yet
-    const toggledSideBar = useUserLocalStorage("user-store-toggled-side-bar", "tools", currentUser);
     const preferredListViewMode = useUserLocalStorage("user-store-preferred-list-view-mode", "grid", currentUser);
 
     let loadPromise: Promise<void> | null = null;
@@ -132,10 +130,6 @@ export const useUserStore = defineStore("userStore", () => {
         preferredListViewMode.value = view;
     }
 
-    function toggleSideBar(currentOpen = "") {
-        toggledSideBar.value = toggledSideBar.value === currentOpen ? "" : currentOpen;
-    }
-
     function processUserPreferences(user: RegisteredUser): Preferences {
         // Favorites are returned as a JSON string by the API
         const favorites =
@@ -153,7 +147,6 @@ export const useUserStore = defineStore("userStore", () => {
         isAnonymous,
         currentTheme,
         currentFavorites,
-        toggledSideBar,
         preferredListViewMode,
         loadUser,
         matchesCurrentUsername,
@@ -162,7 +155,6 @@ export const useUserStore = defineStore("userStore", () => {
         setPreferredListViewMode,
         addFavoriteTool,
         removeFavoriteTool,
-        toggleSideBar,
         $reset,
     };
 });

From 56946f938578230f2b332378dc628196dd9d6aff Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 22 Aug 2024 16:32:27 +0200
Subject: [PATCH 004/131] add save and exit activity

---
 .../components/ActivityBar/ActivityBar.vue    | 46 ++++++++++++-------
 .../ActivityBar/Items/InteractiveItem.vue     |  2 +-
 .../ActivityBar/Items/UploadItem.vue          |  4 +-
 .../src/components/Workflow/Editor/Index.vue  | 14 +++++-
 .../Workflow/Editor/modules/activities.ts     | 25 +++++-----
 client/src/stores/activityStore.ts            | 28 +++++------
 6 files changed, 73 insertions(+), 46 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 052ef9091491..851103fe6779 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -63,7 +63,10 @@ watchImmediate(
 
 const { isAdmin, isAnonymous } = storeToRefs(userStore);
 
-const emit = defineEmits(["dragstart"]);
+const emit = defineEmits<{
+    (e: "dragstart", dragItem: Activity | null): void;
+    (e: "activityClicked", activityId: string): void;
+}>();
 
 // activities from store
 const { activities } = storeToRefs(activityStore);
@@ -78,7 +81,7 @@ const isDragging = ref(false);
 /**
  * Checks if the route of an activity is currently being visited and panels are collapsed
  */
-function isActiveRoute(activityTo: string) {
+function isActiveRoute(activityTo?: string | null) {
     return route.path === activityTo && isActiveSideBar("");
 }
 
@@ -95,7 +98,10 @@ const isSideBarOpen = computed(() => activityStore.toggledSideBar !== "");
  * Checks if an activity that has a panel should have the `is-active` prop
  */
 function panelActivityIsActive(activity: Activity) {
-    return isActiveSideBar(activity.id) || (activity.to !== null && isActiveRoute(activity.to));
+    return (
+        isActiveSideBar(activity.id) ||
+        (activity.to !== undefined && activity.to !== null && isActiveRoute(activity.to))
+    );
 }
 
 /**
@@ -145,7 +151,7 @@ function onDragOver(evt: MouseEvent) {
 /**
  * Tracks the state of activities which expand or collapse the sidepanel
  */
-function onToggleSidebar(toggle: string = "", to: string | null = null) {
+function toggleSidebar(toggle: string = "", to: string | null = null) {
     // if an activity's dedicated panel/sideBar is already active
     // but the route is different, don't collapse
     if (toggle && to && !(route.path === to) && isActiveSideBar(toggle)) {
@@ -154,6 +160,14 @@ function onToggleSidebar(toggle: string = "", to: string | null = null) {
     activityStore.toggleSideBar(toggle);
 }
 
+function onActivityClicked(activity: Activity) {
+    if (activity.click) {
+        emit("activityClicked", activity.id);
+    } else {
+        toggleSidebar();
+    }
+}
+
 defineExpose({
     isActiveSideBar,
 });
@@ -196,7 +210,7 @@ defineExpose({
                                 :title="activity.title"
                                 :tooltip="activity.tooltip"
                                 :to="activity.to"
-                                @click="onToggleSidebar()" />
+                                @click="toggleSidebar()" />
                             <ActivityItem
                                 v-else-if="activity.id === 'admin' || activity.panel"
                                 :id="`activity-${activity.id}`"
@@ -206,17 +220,17 @@ defineExpose({
                                 :title="activity.title"
                                 :tooltip="activity.tooltip"
                                 :to="activity.to || ''"
-                                @click="onToggleSidebar(activity.id, activity.to)" />
+                                @click="toggleSidebar(activity.id, activity.to)" />
                             <ActivityItem
-                                v-else-if="activity.to"
+                                v-else
                                 :id="`activity-${activity.id}`"
                                 :key="activity.id"
                                 :icon="activity.icon"
                                 :is-active="isActiveRoute(activity.to)"
                                 :title="activity.title"
                                 :tooltip="activity.tooltip"
-                                :to="activity.to"
-                                @click="onToggleSidebar()" />
+                                :to="activity.to ?? undefined"
+                                @click="onActivityClicked(activity)" />
                         </div>
                     </div>
                 </draggable>
@@ -228,14 +242,14 @@ defineExpose({
                     icon="bell"
                     :is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
                     title="Notifications"
-                    @click="onToggleSidebar('notifications')" />
+                    @click="toggleSidebar('notifications')" />
                 <ActivityItem
                     id="activity-settings"
                     icon="ellipsis-h"
                     :is-active="isActiveSideBar('settings')"
                     title="More"
                     tooltip="View additional activities"
-                    @click="onToggleSidebar('settings')" />
+                    @click="toggleSidebar('settings')" />
                 <ActivityItem
                     v-if="isAdmin && showAdmin"
                     id="activity-admin"
@@ -244,7 +258,7 @@ defineExpose({
                     title="Admin"
                     tooltip="Administer this Galaxy"
                     variant="danger"
-                    @click="onToggleSidebar('admin')" />
+                    @click="toggleSidebar('admin')" />
                 <template v-for="activity in props.specialActivities">
                     <ActivityItem
                         v-if="activity.panel"
@@ -255,17 +269,17 @@ defineExpose({
                         :title="activity.title"
                         :tooltip="activity.tooltip"
                         :to="activity.to || ''"
-                        @click="onToggleSidebar(activity.id, activity.to)" />
+                        @click="toggleSidebar(activity.id, activity.to)" />
                     <ActivityItem
-                        v-else-if="activity.to"
+                        v-else
                         :id="`activity-${activity.id}`"
                         :key="activity.id"
                         :icon="activity.icon"
                         :is-active="isActiveRoute(activity.to)"
                         :title="activity.title"
                         :tooltip="activity.tooltip"
-                        :to="activity.to"
-                        @click="onToggleSidebar()" />
+                        :to="activity.to ?? undefined"
+                        @click="onActivityClicked(activity)" />
                 </template>
             </b-nav>
         </div>
diff --git a/client/src/components/ActivityBar/Items/InteractiveItem.vue b/client/src/components/ActivityBar/Items/InteractiveItem.vue
index 31b9324e93a0..15aa604b3f13 100644
--- a/client/src/components/ActivityBar/Items/InteractiveItem.vue
+++ b/client/src/components/ActivityBar/Items/InteractiveItem.vue
@@ -13,7 +13,7 @@ const totalCount = computed(() => entryPoints.value.length);
 export interface Props {
     id: string;
     title: string;
-    icon: string;
+    icon: string | object;
     isActive: boolean;
     to: string;
 }
diff --git a/client/src/components/ActivityBar/Items/UploadItem.vue b/client/src/components/ActivityBar/Items/UploadItem.vue
index e5c2b357d0e7..2a5fbf7521db 100644
--- a/client/src/components/ActivityBar/Items/UploadItem.vue
+++ b/client/src/components/ActivityBar/Items/UploadItem.vue
@@ -6,12 +6,12 @@ import { useGlobalUploadModal } from "@/composables/globalUploadModal.js";
 import { useUploadStore } from "@/stores/uploadStore";
 import Query from "@/utils/query-string-parsing.js";
 
-import ActivityItem from "components/ActivityBar/ActivityItem.vue";
+import ActivityItem from "@/components/ActivityBar/ActivityItem.vue";
 
 export interface Props {
     id: string;
     title: string;
-    icon: string;
+    icon: string | object;
     tooltip: string;
 }
 
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 677e36ea2e02..18e26704151d 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -31,7 +31,8 @@
             :default-activities="workflowEditorActivities"
             :special-activities="specialWorkflowActivities"
             activity-bar-id="workflow-editor"
-            :show-admin="false">
+            :show-admin="false"
+            @activityClicked="onActivityClicked">
             <template v-slot:side-panel="{ isActiveSideBar }">
                 <ToolPanel
                     v-if="isActiveSideBar('workflow-editor-tools')"
@@ -730,6 +731,17 @@ export default {
         onSaveAs() {
             this.showSaveAsModal = true;
         },
+        async onActivityClicked(activityId) {
+            if (activityId === "save-and-exit") {
+                if (this.isNewTempWorkflow) {
+                    await this.onCreate();
+                } else {
+                    await this.onSave();
+                }
+
+                this.$router.push("/");
+            }
+        },
         onLayout() {
             return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => {
                 layout.autoLayout(this.id, this.steps).then((newSteps) => {
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index ccfe7e12af24..fd2686ec6bba 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,17 +1,17 @@
+import { faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
+
 import type { Activity } from "@/stores/activityStore";
 
 export const workflowEditorActivities = [
     {
         anonymous: true,
-        description: "Displays the tool panel to search and access all available tools.",
-        icon: "wrench",
+        description: "Displays the tool panel to search and place all available tools.",
+        icon: faWrench,
         id: "workflow-editor-tools",
-        mutable: false,
-        optional: false,
         panel: true,
         title: "Tools",
         to: null,
-        tooltip: "Search and run tools",
+        tooltip: "Search tools to use in your workflow",
         visible: true,
     },
 ] as const satisfies Readonly<Activity[]>;
@@ -19,15 +19,16 @@ export const workflowEditorActivities = [
 export const specialWorkflowActivities = [
     {
         anonymous: true,
-        description: "Displays the tool panel to search and access all available tools.",
-        icon: "wrench",
-        id: "tools",
+        description: "Save this workflow, then exit the workflow editor.",
+        icon: faSave,
+        id: "save-and-exit",
+        title: "Save + Exit",
+        to: null,
+        tooltip: "Save and Exit",
         mutable: false,
         optional: false,
-        panel: true,
-        title: "Tools",
-        to: null,
-        tooltip: "Search and run tools",
+        panel: false,
         visible: true,
+        click: true,
     },
 ] as const satisfies Readonly<Activity[]>;
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index 77306911bbb1..6ee776543937 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -12,27 +12,27 @@ import { defineScopedStore } from "./scopedStore";
 
 export interface Activity {
     // determine wether an anonymous user can access this activity
-    anonymous: boolean;
+    anonymous?: boolean;
     // description of the activity
     description: string;
     // unique identifier
     id: string;
     // icon to be displayed in activity bar
-    icon: string;
+    icon: string | object;
     // indicate if this activity can be modified and/or deleted
-    mutable: boolean;
+    mutable?: boolean;
     // indicate wether this activity can be disabled by the user
-    optional: boolean;
+    optional?: boolean;
     // specifiy wether this activity utilizes the side panel
-    panel: boolean;
+    panel?: boolean;
     // title to be displayed in the activity bar
     title: string;
     // route to be executed upon selecting the activity
-    to: string | null;
+    to?: string | null;
     // tooltip to be displayed when hovering above the icon
     tooltip: string;
     // indicate wether the activity should be visible by default
-    visible: boolean;
+    visible?: boolean;
     // if activity should cause a click event
     click?: true;
 }
@@ -51,13 +51,6 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         toggledSideBar.value = toggledSideBar.value === currentOpen ? "" : currentOpen;
     }
 
-    watchImmediate(
-        () => hashedUserId.value,
-        () => {
-            sync();
-        }
-    );
-
     function overrideDefaultActivities(activities: Activity[]) {
         customDefaultActivities.value = activities;
         sync();
@@ -158,6 +151,13 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         }
     }
 
+    watchImmediate(
+        () => hashedUserId.value,
+        () => {
+            sync();
+        }
+    );
+
     return {
         toggledSideBar,
         toggleSideBar,

From b73905b88f49fcd6cac3630142e54dfc87b490bf Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 22 Aug 2024 16:34:39 +0200
Subject: [PATCH 005/131] route to workflow list on exit

---
 client/src/components/Workflow/Editor/Index.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 18e26704151d..ec869991b41d 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -739,7 +739,7 @@ export default {
                     await this.onSave();
                 }
 
-                this.$router.push("/");
+                this.$router.push("/workflows/list");
             }
         },
         onLayout() {

From 3283d6a97abbd9bdb5b965581e8c894d95998fea Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 22 Aug 2024 22:12:11 +0200
Subject: [PATCH 006/131] move best practices to activity bar

---
 .../src/components/Workflow/Editor/Index.vue  | 33 ++++++++-----------
 .../components/Workflow/Editor/Options.vue    |  8 +----
 .../Workflow/Editor/modules/activities.ts     | 19 +++++++----
 3 files changed, 27 insertions(+), 33 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index ec869991b41d..a6a3e22bda86 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -44,6 +44,19 @@
                     @onInsertModule="onInsertModule"
                     @onInsertWorkflow="onInsertWorkflow"
                     @onInsertWorkflowSteps="onInsertWorkflowSteps" />
+                <WorkflowLint
+                    v-else-if="isActiveSideBar('workflow-best-practices')"
+                    :untyped-parameters="parameters"
+                    :annotation="annotation"
+                    :creator="creator"
+                    :license="license"
+                    :steps="steps"
+                    :datatypes-mapper="datatypesMapper"
+                    @onAttributes="() => showAttributes(true)"
+                    @onHighlight="onHighlight"
+                    @onUnhighlight="onUnhighlight"
+                    @onRefactor="onAttemptRefactor"
+                    @onScrollTo="onScrollTo" />
             </template>
         </ActivityBar>
         <div id="center" class="workflow-center">
@@ -111,7 +124,6 @@
                             @onLayout="onLayout"
                             @onEdit="onEdit"
                             @onAttributes="() => showAttributes(true)"
-                            @onLint="onLint"
                             @onUpgrade="onUpgrade" />
                     </div>
                 </div>
@@ -155,19 +167,6 @@
                             @onCreator="onCreator"
                             @update:nameCurrent="setName"
                             @update:annotationCurrent="setAnnotation" />
-                        <WorkflowLint
-                            v-else-if="showInPanel === 'lint'"
-                            :untyped-parameters="parameters"
-                            :annotation="annotation"
-                            :creator="creator"
-                            :license="license"
-                            :steps="steps"
-                            :datatypes-mapper="datatypesMapper"
-                            @onAttributes="() => showAttributes(true)"
-                            @onHighlight="onHighlight"
-                            @onUnhighlight="onUnhighlight"
-                            @onRefactor="onAttemptRefactor"
-                            @onScrollTo="onScrollTo" />
                     </div>
                 </div>
             </div>
@@ -821,12 +820,6 @@ export default {
         onUnhighlight(stepId) {
             this.highlightId = null;
         },
-        onLint() {
-            this.ensureParametersSet();
-            this.stateStore.activeNodeId = null;
-            this.showInPanel = "lint";
-            this.showChanges = false;
-        },
         onUpgrade() {
             this.onAttemptRefactor([{ action_type: "upgrade_all_steps" }]);
         },
diff --git a/client/src/components/Workflow/Editor/Options.vue b/client/src/components/Workflow/Editor/Options.vue
index e288489a0364..d7e395d63a07 100644
--- a/client/src/components/Workflow/Editor/Options.vue
+++ b/client/src/components/Workflow/Editor/Options.vue
@@ -7,7 +7,6 @@ import {
     faDownload,
     faEdit,
     faHistory,
-    faMagic,
     faPencilAlt,
     faPlay,
     faRecycle,
@@ -18,7 +17,7 @@ import { computed } from "vue";
 
 import { useConfirmDialog } from "@/composables/confirmDialog";
 
-library.add(faPencilAlt, faEdit, faCog, faAlignLeft, faMagic, faDownload, faPlay, faHistory, faSave, faRecycle);
+library.add(faPencilAlt, faEdit, faCog, faAlignLeft, faDownload, faPlay, faHistory, faSave, faRecycle);
 
 const emit = defineEmits<{
     (e: "onAttributes"): void;
@@ -27,7 +26,6 @@ const emit = defineEmits<{
     (e: "onReport"): void;
     (e: "onSaveAs"): void;
     (e: "onLayout"): void;
-    (e: "onLint"): void;
     (e: "onUpgrade"): void;
     (e: "onDownload"): void;
     (e: "onRun"): void;
@@ -144,10 +142,6 @@ async function onSave() {
                 <FontAwesomeIcon icon="fa fa-align-left" /> Auto Layout
             </BDropdownItem>
 
-            <BDropdownItem href="#" @click="emit('onLint')">
-                <FontAwesomeIcon icon="fa fa-magic" /> Best Practices
-            </BDropdownItem>
-
             <BDropdownItem href="#" @click="emit('onUpgrade')">
                 <FontAwesomeIcon icon="fa fa-recycle" /> Upgrade Workflow
             </BDropdownItem>
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index fd2686ec6bba..316f4d6a8f28 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,24 +1,31 @@
-import { faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
+import { faMagic, faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
 
 import type { Activity } from "@/stores/activityStore";
 
 export const workflowEditorActivities = [
     {
-        anonymous: true,
+        title: "Tools",
+        id: "workflow-editor-tools",
         description: "Displays the tool panel to search and place all available tools.",
         icon: faWrench,
-        id: "workflow-editor-tools",
         panel: true,
-        title: "Tools",
-        to: null,
         tooltip: "Search tools to use in your workflow",
         visible: true,
     },
+    {
+        title: "Best Practices",
+        id: "workflow-best-practices",
+        description: "Show and test for the best practices in this workflow.",
+        tooltip: "Test workflow for best practices",
+        icon: faMagic,
+        panel: true,
+        visible: true,
+        optional: true,
+    },
 ] as const satisfies Readonly<Activity[]>;
 
 export const specialWorkflowActivities = [
     {
-        anonymous: true,
         description: "Save this workflow, then exit the workflow editor.",
         icon: faSave,
         id: "save-and-exit",

From a14191c8d0d10da85f799dbba852f16b2e9b2638 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 23 Aug 2024 13:54:05 +0200
Subject: [PATCH 007/131] use ActivityPanel component for Lint

---
 .../src/components/Workflow/Editor/Lint.vue   | 119 +++++++++---------
 1 file changed, 57 insertions(+), 62 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Lint.vue b/client/src/components/Workflow/Editor/Lint.vue
index 86b80a1c392c..4638398ffd79 100644
--- a/client/src/components/Workflow/Editor/Lint.vue
+++ b/client/src/components/Workflow/Editor/Lint.vue
@@ -1,77 +1,69 @@
 <template>
-    <b-card id="lint-panel" header-tag="header" body-class="p-0" class="right-content">
-        <template v-slot:header>
-            <div class="mb-1 font-weight-bold">
-                <FontAwesomeIcon icon="magic" class="mr-1" />
-                Best Practices Review
-            </div>
-            <div v-if="showRefactor">
-                <a class="refactor-button" href="#" @click="onRefactor"> Try to automatically fix issues. </a>
-            </div>
+    <ActivityPanel title="Best Practices Review">
+        <template v-if="showRefactor" v-slot:header>
+            <button class="refactor-button ui-link" @click="onRefactor">Try to automatically fix issues.</button>
         </template>
-        <b-card-body>
-            <LintSection
-                :okay="checkAnnotation"
-                success-message="This workflow is annotated. Ideally, this helps the executors of the workflow
+        <LintSection
+            :okay="checkAnnotation"
+            success-message="This workflow is annotated. Ideally, this helps the executors of the workflow
                     understand the purpose and usage of the workflow."
-                warning-message="This workflow is not annotated. Providing an annotation helps workflow executors
+            warning-message="This workflow is not annotated. Providing an annotation helps workflow executors
                     understand the purpose and usage of the workflow."
-                attribute-link="Annotate your Workflow."
-                @onClick="onAttributes" />
-            <LintSection
-                :okay="checkCreator"
-                success-message="This workflow defines creator information."
-                warning-message="This workflow does not specify creator(s). This is important metadata for workflows
+            attribute-link="Annotate your Workflow."
+            @onClick="onAttributes" />
+        <LintSection
+            :okay="checkCreator"
+            success-message="This workflow defines creator information."
+            warning-message="This workflow does not specify creator(s). This is important metadata for workflows
                     that will be published and/or shared to help workflow executors know how to cite the
                     workflow authors."
-                attribute-link="Provide Creator Details."
-                @onClick="onAttributes" />
-            <LintSection
-                :okay="checkLicense"
-                success-message="This workflow defines a license."
-                warning-message="This workflow does not specify a license. This is important metadata for workflows
+            attribute-link="Provide Creator Details."
+            @onClick="onAttributes" />
+        <LintSection
+            :okay="checkLicense"
+            success-message="This workflow defines a license."
+            warning-message="This workflow does not specify a license. This is important metadata for workflows
                     that will be published and/or shared to help workflow executors understand how it
                     may be used."
-                attribute-link="Specify a License."
-                @onClick="onAttributes" />
-            <LintSection
-                success-message="Workflow parameters are using formal input parameters."
-                warning-message="This workflow uses legacy workflow parameters. They should be replaced with
+            attribute-link="Specify a License."
+            @onClick="onAttributes" />
+        <LintSection
+            success-message="Workflow parameters are using formal input parameters."
+            warning-message="This workflow uses legacy workflow parameters. They should be replaced with
                 formal workflow inputs. Formal input parameters make tracking workflow provenance, usage within subworkflows,
                 and executing the workflow via the API more robust:"
-                :warning-items="warningUntypedParameters"
-                @onMouseOver="onHighlight"
-                @onMouseLeave="onUnhighlight"
-                @onClick="onFixUntypedParameter" />
-            <LintSection
-                success-message="All non-optional inputs to workflow steps are connected to formal input parameters."
-                warning-message="Some non-optional inputs are not connected to formal workflow inputs. Formal input parameters
+            :warning-items="warningUntypedParameters"
+            @onMouseOver="onHighlight"
+            @onMouseLeave="onUnhighlight"
+            @onClick="onFixUntypedParameter" />
+        <LintSection
+            success-message="All non-optional inputs to workflow steps are connected to formal input parameters."
+            warning-message="Some non-optional inputs are not connected to formal workflow inputs. Formal input parameters
                 make tracking workflow provenance, usage within subworkflows, and executing the workflow via the API more robust:"
-                :warning-items="warningDisconnectedInputs"
-                @onMouseOver="onHighlight"
-                @onMouseLeave="onUnhighlight"
-                @onClick="onFixDisconnectedInput" />
-            <LintSection
-                success-message="All workflow inputs have labels and annotations."
-                warning-message="Some workflow inputs are missing labels and/or annotations:"
-                :warning-items="warningMissingMetadata"
-                @onMouseOver="onHighlight"
-                @onMouseLeave="onUnhighlight"
-                @onClick="onScrollTo" />
-            <LintSection
-                success-message="This workflow has outputs and they all have valid labels."
-                warning-message="The following workflow outputs have no labels, they should be assigned a useful label or
+            :warning-items="warningDisconnectedInputs"
+            @onMouseOver="onHighlight"
+            @onMouseLeave="onUnhighlight"
+            @onClick="onFixDisconnectedInput" />
+        <LintSection
+            success-message="All workflow inputs have labels and annotations."
+            warning-message="Some workflow inputs are missing labels and/or annotations:"
+            :warning-items="warningMissingMetadata"
+            @onMouseOver="onHighlight"
+            @onMouseLeave="onUnhighlight"
+            @onClick="onScrollTo" />
+        <LintSection
+            success-message="This workflow has outputs and they all have valid labels."
+            warning-message="The following workflow outputs have no labels, they should be assigned a useful label or
                     unchecked in the workflow editor to mark them as no longer being a workflow output:"
-                :warning-items="warningUnlabeledOutputs"
-                @onMouseOver="onHighlight"
-                @onMouseLeave="onUnhighlight"
-                @onClick="onFixUnlabeledOutputs" />
-            <div v-if="!hasActiveOutputs">
-                <FontAwesomeIcon icon="exclamation-triangle" class="text-warning" />
-                <span>This workflow has no labeled outputs, please select and label at least one output.</span>
-            </div>
-        </b-card-body>
-    </b-card>
+            :warning-items="warningUnlabeledOutputs"
+            @onMouseOver="onHighlight"
+            @onMouseLeave="onUnhighlight"
+            @onClick="onFixUnlabeledOutputs" />
+        <div v-if="!hasActiveOutputs">
+            <FontAwesomeIcon icon="exclamation-triangle" class="text-warning" />
+            <span>This workflow has no labeled outputs, please select and label at least one output.</span>
+        </div>
+    </ActivityPanel>
 </template>
 
 <script>
@@ -98,6 +90,8 @@ import {
     getUntypedParameters,
 } from "./modules/linting";
 
+import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
+
 Vue.use(BootstrapVue);
 
 library.add(faExclamationTriangle);
@@ -107,6 +101,7 @@ export default {
     components: {
         FontAwesomeIcon,
         LintSection,
+        ActivityPanel,
     },
     props: {
         untypedParameters: {

From a3226ae84fc6089710cadc38b0b03b6f83261966 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 23 Aug 2024 14:04:44 +0200
Subject: [PATCH 008/131] move UndoRedoStack to activity bar

---
 .../src/components/UndoRedo/UndoRedoStack.vue | 12 ++------
 .../src/components/Workflow/Editor/Index.vue  | 28 ++++---------------
 .../Workflow/Editor/modules/activities.ts     | 12 +++++++-
 3 files changed, 19 insertions(+), 33 deletions(-)

diff --git a/client/src/components/UndoRedo/UndoRedoStack.vue b/client/src/components/UndoRedo/UndoRedoStack.vue
index e0b89b1cbe73..80de89bc6e94 100644
--- a/client/src/components/UndoRedo/UndoRedoStack.vue
+++ b/client/src/components/UndoRedo/UndoRedoStack.vue
@@ -1,13 +1,9 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faHistory } from "@fortawesome/free-solid-svg-icons";
 import { ref, watch } from "vue";
 
 import { useUndoRedoStore } from "@/stores/undoRedoStore";
 
-import Heading from "@/components/Common/Heading.vue";
-
-library.add(faHistory);
+import ActivityPanel from "../Panels/ActivityPanel.vue";
 
 const props = defineProps<{
     storeId: string;
@@ -37,9 +33,7 @@ function updateSavedUndoActions() {
 </script>
 
 <template>
-    <section class="undo-redo-stack">
-        <Heading h2 size="sm" icon="fa-history">Latest Changes</Heading>
-
+    <ActivityPanel title="Latest Changes" class="undo-redo-stack">
         <div class="scroll-list">
             <button
                 v-for="action in currentStore.redoActionStack"
@@ -87,7 +81,7 @@ function updateSavedUndoActions() {
                 @focusout="updateSavedUndoActions"
                 @keyup.enter="updateSavedUndoActions" />
         </label>
-    </section>
+    </ActivityPanel>
 </template>
 
 <style scoped lang="scss">
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index a6a3e22bda86..25001ca8c418 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -52,11 +52,12 @@
                     :license="license"
                     :steps="steps"
                     :datatypes-mapper="datatypesMapper"
-                    @onAttributes="() => showAttributes(true)"
+                    @onAttributes="showAttributes"
                     @onHighlight="onHighlight"
                     @onUnhighlight="onUnhighlight"
                     @onRefactor="onAttemptRefactor"
                     @onScrollTo="onScrollTo" />
+                <UndoRedoStack v-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
             </template>
         </ActivityBar>
         <div id="center" class="workflow-center">
@@ -82,12 +83,6 @@
                         @click="undoRedoStore.redo()">
                         <FontAwesomeIcon icon="fa-arrow-right" />
                     </b-button>
-                    <b-button
-                        title="View Last Changes"
-                        :variant="showChanges ? 'primary' : 'link'"
-                        @click="toggleShowChanges">
-                        <FontAwesomeIcon icon="fa-history" />
-                    </b-button>
                 </b-button-group>
             </div>
             <WorkflowGraph
@@ -123,15 +118,14 @@
                             @onReport="onReport"
                             @onLayout="onLayout"
                             @onEdit="onEdit"
-                            @onAttributes="() => showAttributes(true)"
+                            @onAttributes="showAttributes"
                             @onUpgrade="onUpgrade" />
                     </div>
                 </div>
                 <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
                     <div v-if="!initialLoading" class="position-relative h-100">
-                        <UndoRedoStack v-if="showChanges" :store-id="id" />
                         <FormTool
-                            v-else-if="hasActiveNodeTool"
+                            v-if="hasActiveNodeTool"
                             :key="activeStep.id"
                             :step="activeStep"
                             :datatypes="datatypes"
@@ -314,18 +308,8 @@ export default {
         }
 
         const showInPanel = ref("attributes");
-        const showChanges = ref(false);
-
-        function toggleShowChanges() {
-            ensureParametersSet();
-            showChanges.value = !showChanges.value;
-        }
-
-        function showAttributes(closeChanges = false) {
-            if (closeChanges) {
-                showChanges.value = false;
-            }
 
+        function showAttributes() {
             ensureParametersSet();
             stateStore.activeNodeId = null;
             showInPanel.value = "attributes";
@@ -474,8 +458,6 @@ export default {
             parameters,
             ensureParametersSet,
             showInPanel,
-            showChanges,
-            toggleShowChanges,
             showAttributes,
             setName,
             report,
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index 316f4d6a8f28..8a29a8847bec 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,4 +1,4 @@
-import { faMagic, faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
+import { faHistory, faMagic, faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
 
 import type { Activity } from "@/stores/activityStore";
 
@@ -22,6 +22,16 @@ export const workflowEditorActivities = [
         visible: true,
         optional: true,
     },
+    {
+        title: "Changes",
+        id: "workflow-undo-redo",
+        description: "View, undo, and redo your latest changes.",
+        tooltip: "Show and manage latest changes",
+        icon: faHistory,
+        panel: true,
+        visible: true,
+        optional: true,
+    },
 ] as const satisfies Readonly<Activity[]>;
 
 export const specialWorkflowActivities = [

From 306ef42cf583645f71dd7e741429d41de692616a Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 23 Aug 2024 15:25:11 +0200
Subject: [PATCH 009/131] add placeholder activities

---
 .../Workflow/Editor/modules/activities.ts     | 39 ++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index 8a29a8847bec..ee69a02b40d8 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,8 +1,25 @@
-import { faHistory, faMagic, faSave, faWrench } from "@fortawesome/free-solid-svg-icons";
+import {
+    faHistory,
+    faMagic,
+    faPencilAlt,
+    faSave,
+    faSitemap,
+    faTools,
+    faWrench,
+} from "@fortawesome/free-solid-svg-icons";
 
 import type { Activity } from "@/stores/activityStore";
 
 export const workflowEditorActivities = [
+    {
+        title: "Attributes",
+        id: "workflow-editor-attributes",
+        tooltip: "Edit workflow attributes",
+        description: "View and edit the attributes of this workflow.",
+        panel: true,
+        icon: faPencilAlt,
+        visible: true,
+    },
     {
         title: "Tools",
         id: "workflow-editor-tools",
@@ -12,6 +29,26 @@ export const workflowEditorActivities = [
         tooltip: "Search tools to use in your workflow",
         visible: true,
     },
+    {
+        title: "Workflow Tools",
+        id: "workflow-editor-utility-tools",
+        description: "Browse and insert tools specific to workflows.",
+        tooltip: "Workflow specific tools",
+        icon: faTools,
+        panel: true,
+        optional: true,
+        visible: false,
+    },
+    {
+        title: "Workflows",
+        id: "workflow-editor-workflows",
+        description: "Browse other workflows and add them as sub-workflows.",
+        tooltip: "Search workflows to use in your workflow",
+        icon: faSitemap,
+        panel: true,
+        visible: true,
+        optional: true,
+    },
     {
         title: "Best Practices",
         id: "workflow-best-practices",

From 30df771c01f41556a67408497d7853f1e8689d49 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 28 Aug 2024 11:26:04 +0200
Subject: [PATCH 010/131] fix delayed input make delayed input v-model
 compliant

---
 client/src/components/Common/DelayedInput.vue | 41 ++++++++-----------
 client/src/components/Common/FilterMenu.vue   |  2 +-
 .../History/Archiving/HistoryArchive.vue      |  2 +-
 .../components/Panels/Common/ToolSearch.vue   |  2 +-
 client/src/components/Tour/TourList.vue       |  2 +-
 .../components/Visualizations/PluginList.vue  |  2 +-
 6 files changed, 23 insertions(+), 28 deletions(-)

diff --git a/client/src/components/Common/DelayedInput.vue b/client/src/components/Common/DelayedInput.vue
index 490c33de45d1..c22fa9885866 100644
--- a/client/src/components/Common/DelayedInput.vue
+++ b/client/src/components/Common/DelayedInput.vue
@@ -2,15 +2,17 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faAngleDoubleDown, faAngleDoubleUp, faSpinner, faTimes } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+import { watchImmediate } from "@vueuse/core";
 import { BButton, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue";
-import { onMounted, ref, watch } from "vue";
+import { computed, ref } from "vue";
 
 import localize from "@/utils/localization";
 
 library.add(faAngleDoubleDown, faAngleDoubleUp, faSpinner, faTimes);
 
 interface Props {
-    query?: string;
+    modelValue?: string;
+    value?: string;
     delay?: number;
     loading?: boolean;
     placeholder?: string;
@@ -19,7 +21,8 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    query: "",
+    modelValue: "",
+    value: "",
     delay: 1000,
     loading: false,
     placeholder: "Enter your search term here.",
@@ -27,14 +30,16 @@ const props = withDefaults(defineProps<Props>(), {
     enableAdvanced: false,
 });
 
+const currentValue = computed(() => props.modelValue ?? props.value ?? "");
+
 const emit = defineEmits<{
-    (e: "change", query: string): void;
+    (e: "update:modelValue", value: string): void;
+    (e: "change", value: string): void;
     (e: "onToggle", showAdvanced: boolean): void;
 }>();
 
 const queryInput = ref<string>();
-const queryCurrent = ref<string>();
-const queryTimer = ref<any>(null);
+const queryTimer = ref<ReturnType<typeof setTimeout> | null>(null);
 const titleClear = ref("Clear Search (esc)");
 const titleAdvanced = ref("Toggle Advanced Search");
 const toolInput = ref<HTMLInputElement | null>(null);
@@ -57,16 +62,13 @@ function delayQuery(query: string) {
 }
 
 function setQuery(queryNew: string) {
-    clearTimer();
-
-    if (queryCurrent.value !== queryInput.value || queryCurrent.value !== queryNew) {
-        queryCurrent.value = queryInput.value = queryNew;
-        emit("change", queryCurrent.value);
-    }
+    emit("update:modelValue", queryNew);
+    emit("change", queryNew);
 }
 
 function clearBox() {
     setQuery("");
+    queryInput.value = "";
     toolInput.value?.focus();
 }
 
@@ -74,18 +76,12 @@ function onToggle() {
     emit("onToggle", !props.showAdvanced);
 }
 
-watch(
-    () => props.query,
+watchImmediate(
+    () => currentValue.value,
     (newQuery) => {
-        setQuery(newQuery);
+        queryInput.value = newQuery;
     }
 );
-
-onMounted(() => {
-    if (props.query) {
-        setQuery(props.query);
-    }
-});
 </script>
 
 <template>
@@ -99,8 +95,7 @@ onMounted(() => {
             :placeholder="placeholder"
             data-description="filter text input"
             @input="delayQuery"
-            @change="setQuery"
-            @keydown.esc="setQuery('')" />
+            @keydown.esc="clearBox" />
 
         <BInputGroupAppend>
             <BButton
diff --git a/client/src/components/Common/FilterMenu.vue b/client/src/components/Common/FilterMenu.vue
index 66a5e4c823f4..6686a141471a 100644
--- a/client/src/components/Common/FilterMenu.vue
+++ b/client/src/components/Common/FilterMenu.vue
@@ -207,7 +207,7 @@ function updateFilterText(newFilterText: string) {
             v-if="props.menuType !== 'standalone'"
             v-show="props.menuType == 'linked' || (props.menuType == 'separate' && !props.showAdvanced)"
             ref="delayedInputField"
-            :query="props.filterText"
+            :value="props.filterText"
             :delay="props.debounceDelay"
             :loading="props.loading"
             :show-advanced="props.showAdvanced"
diff --git a/client/src/components/History/Archiving/HistoryArchive.vue b/client/src/components/History/Archiving/HistoryArchive.vue
index 22477bfb6b5b..7db329b99c4d 100644
--- a/client/src/components/History/Archiving/HistoryArchive.vue
+++ b/client/src/components/History/Archiving/HistoryArchive.vue
@@ -151,7 +151,7 @@ async function onImportCopy(history: ArchivedHistorySummary) {
     <section id="archived-histories" class="d-flex flex-column">
         <div>
             <DelayedInput
-                :query="searchText"
+                :value="searchText"
                 class="m-1 mb-3"
                 placeholder="search by name"
                 @change="updateSearchQuery" />
diff --git a/client/src/components/Panels/Common/ToolSearch.vue b/client/src/components/Panels/Common/ToolSearch.vue
index 70b776fc7fc9..818d6259d875 100644
--- a/client/src/components/Panels/Common/ToolSearch.vue
+++ b/client/src/components/Panels/Common/ToolSearch.vue
@@ -245,7 +245,7 @@ function onAdvancedSearch(filters: any) {
         <DelayedInput
             v-else
             class="mb-3"
-            :query="props.query"
+            :value="props.query"
             :delay="200"
             :loading="queryPending"
             :placeholder="placeholder"
diff --git a/client/src/components/Tour/TourList.vue b/client/src/components/Tour/TourList.vue
index 5f9f98f1a6c0..145a006ee48c 100644
--- a/client/src/components/Tour/TourList.vue
+++ b/client/src/components/Tour/TourList.vue
@@ -8,7 +8,7 @@
         <h2 class="h-sm">Tours</h2>
         <div v-if="error" class="alert alert-danger">{{ error }}</div>
         <div v-else>
-            <DelayedInput class="mb-3" :query="search" :placeholder="searchTours" :delay="0" @change="onSearch" />
+            <DelayedInput class="mb-3" :value="search" :placeholder="searchTours" :delay="0" @change="onSearch" />
             <div v-for="tour in tours" :key="tour.id">
                 <ul v-if="match(tour)" id="tourList" class="list-group">
                     <li class="list-group-item">
diff --git a/client/src/components/Visualizations/PluginList.vue b/client/src/components/Visualizations/PluginList.vue
index 987858f06fa5..7245bb38eaf5 100644
--- a/client/src/components/Visualizations/PluginList.vue
+++ b/client/src/components/Visualizations/PluginList.vue
@@ -4,7 +4,7 @@
         <template v-else>
             <DelayedInput
                 class="mb-3"
-                :query="search"
+                :value="search"
                 :placeholder="titleSearchVisualizations"
                 :delay="100"
                 @change="onSearch" />

From d35a330cd2d89f976def1f9768ff963b6cfaa679 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 28 Aug 2024 12:27:36 +0200
Subject: [PATCH 011/131] make favorites button reusable

---
 .../Panels/Buttons/FavoritesButton.vue        | 31 ++++++++++++-------
 client/src/components/Panels/ToolPanel.vue    | 10 +++++-
 2 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/client/src/components/Panels/Buttons/FavoritesButton.vue b/client/src/components/Panels/Buttons/FavoritesButton.vue
index c7d285bb4253..190e8d23fdb3 100644
--- a/client/src/components/Panels/Buttons/FavoritesButton.vue
+++ b/client/src/components/Panels/Buttons/FavoritesButton.vue
@@ -3,6 +3,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 import { faStar } from "@fortawesome/free-regular-svg-icons";
 import { faStar as faRegStar } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+import { watchImmediate } from "@vueuse/core";
 import { BButton } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { computed, ref, watch } from "vue";
@@ -12,18 +13,29 @@ import { useUserStore } from "@/stores/userStore";
 library.add(faStar, faRegStar);
 
 interface Props {
-    query: string;
+    value?: boolean;
+    modelValue?: boolean;
+    query?: string;
 }
+
 const props = defineProps<Props>();
 
+const currentValue = computed(() => props.value ?? props.modelValue ?? false);
+const toggle = ref(false);
+
+watchImmediate(
+    () => currentValue.value,
+    (val) => (toggle.value = val)
+);
+
 const emit = defineEmits<{
-    (e: "onFavorites", filter: string): void;
+    (e: "change", toggled: boolean): void;
+    (e: "update:modelValue", toggled: boolean): void;
 }>();
 
 const { isAnonymous } = storeToRefs(useUserStore());
 
 const FAVORITES = ["#favorites", "#favs", "#favourites"];
-const toggle = ref(false);
 
 const tooltipText = computed(() => {
     if (isAnonymous.value) {
@@ -40,17 +52,14 @@ const tooltipText = computed(() => {
 watch(
     () => props.query,
     () => {
-        toggle.value = FAVORITES.includes(props.query);
+        toggle.value = FAVORITES.includes(props.query ?? "");
     }
 );
 
-function onFavorites() {
+function toggleFavorites() {
     toggle.value = !toggle.value;
-    if (toggle.value) {
-        emit("onFavorites", "#favorites");
-    } else {
-        emit("onFavorites", "");
-    }
+    emit("update:modelValue", toggle.value);
+    emit("change", toggle.value);
 }
 </script>
 
@@ -63,7 +72,7 @@ function onFavorites() {
         aria-label="Show favorite tools"
         :disabled="isAnonymous"
         :title="tooltipText"
-        @click="onFavorites">
+        @click="toggleFavorites">
         <FontAwesomeIcon :icon="toggle ? faRegStar : faStar" />
     </BButton>
 </template>
diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue
index a478b08f7c7a..e69f51d37591 100644
--- a/client/src/components/Panels/ToolPanel.vue
+++ b/client/src/components/Panels/ToolPanel.vue
@@ -130,6 +130,14 @@ function onInsertWorkflow(workflowId: string | undefined, workflowName: string)
 function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | undefined) {
     emit("onInsertWorkflowSteps", workflowId, workflowStepCount);
 }
+
+function onFavorites(favorites: boolean) {
+    if (favorites) {
+        query.value = "#favorites";
+    } else {
+        query.value = "";
+    }
+}
 </script>
 
 <template>
@@ -169,7 +177,7 @@ function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | u
                     </template>
                 </PanelViewMenu>
                 <div v-if="!showAdvanced" class="panel-header-buttons">
-                    <FavoritesButton :query="query" @onFavorites="(q) => (query = q)" />
+                    <FavoritesButton :query="query" @toggleFavorites="onFavorites" />
                 </div>
             </div>
         </div>

From e8ed57de08417b90d1ef9f8f0a17895d2a19421d Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 29 Aug 2024 11:34:28 +0200
Subject: [PATCH 012/131] refactor WorkflowCard add types fix type errors
 remove redundant code remove any unify naming

---
 client/src/components/Common/DelayedInput.vue | 19 +++++------
 .../src/components/Common/FilterMenu.test.ts  |  2 +-
 .../components/Workflow/List/WorkflowCard.vue | 21 +++++++-----
 .../Workflow/List/WorkflowIndicators.vue      | 14 +++++---
 .../components/Workflow/List/WorkflowList.vue |  6 ++--
 ...{WorkflowFilters.js => workflowFilters.ts} |  6 ++--
 .../components/Workflow/workflows.services.ts | 34 ++++++++++++++++++-
 client/src/utils/filtering.ts                 |  7 ++--
 8 files changed, 77 insertions(+), 32 deletions(-)
 rename client/src/components/Workflow/List/{WorkflowFilters.js => workflowFilters.ts} (98%)

diff --git a/client/src/components/Common/DelayedInput.vue b/client/src/components/Common/DelayedInput.vue
index c22fa9885866..bbd9bc546646 100644
--- a/client/src/components/Common/DelayedInput.vue
+++ b/client/src/components/Common/DelayedInput.vue
@@ -4,14 +4,13 @@ import { faAngleDoubleDown, faAngleDoubleUp, faSpinner, faTimes } from "@fortawe
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { watchImmediate } from "@vueuse/core";
 import { BButton, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue";
-import { computed, ref } from "vue";
+import { ref, watch } from "vue";
 
 import localize from "@/utils/localization";
 
 library.add(faAngleDoubleDown, faAngleDoubleUp, faSpinner, faTimes);
 
 interface Props {
-    modelValue?: string;
     value?: string;
     delay?: number;
     loading?: boolean;
@@ -21,7 +20,6 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    modelValue: "",
     value: "",
     delay: 1000,
     loading: false,
@@ -30,10 +28,8 @@ const props = withDefaults(defineProps<Props>(), {
     enableAdvanced: false,
 });
 
-const currentValue = computed(() => props.modelValue ?? props.value ?? "");
-
 const emit = defineEmits<{
-    (e: "update:modelValue", value: string): void;
+    (e: "input", value: string): void;
     (e: "change", value: string): void;
     (e: "onToggle", showAdvanced: boolean): void;
 }>();
@@ -62,12 +58,16 @@ function delayQuery(query: string) {
 }
 
 function setQuery(queryNew: string) {
-    emit("update:modelValue", queryNew);
+    emit("input", queryNew);
     emit("change", queryNew);
 }
 
+watch(
+    () => queryInput.value,
+    () => delayQuery(queryInput.value ?? "")
+);
+
 function clearBox() {
-    setQuery("");
     queryInput.value = "";
     toolInput.value?.focus();
 }
@@ -77,7 +77,7 @@ function onToggle() {
 }
 
 watchImmediate(
-    () => currentValue.value,
+    () => props.value,
     (newQuery) => {
         queryInput.value = newQuery;
     }
@@ -94,7 +94,6 @@ watchImmediate(
             autocomplete="off"
             :placeholder="placeholder"
             data-description="filter text input"
-            @input="delayQuery"
             @keydown.esc="clearBox" />
 
         <BInputGroupAppend>
diff --git a/client/src/components/Common/FilterMenu.test.ts b/client/src/components/Common/FilterMenu.test.ts
index b9ba3db65b3b..97aaa72cd4da 100644
--- a/client/src/components/Common/FilterMenu.test.ts
+++ b/client/src/components/Common/FilterMenu.test.ts
@@ -4,7 +4,7 @@ import { mount, type Wrapper } from "@vue/test-utils";
 
 import { HistoryFilters } from "@/components/History/HistoryFilters";
 import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
-import { WorkflowFilters } from "@/components/Workflow/List/WorkflowFilters";
+import { WorkflowFilters } from "@/components/Workflow/List/workflowFilters";
 import Filtering, { compare, contains, equals, toBool, toDate } from "@/utils/filtering";
 
 import FilterMenu from "./FilterMenu.vue";
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 585ba221b81e..39cc9748dc6a 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -1,6 +1,5 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faEdit, faEye, faPen, faUpload } from "@fortawesome/free-solid-svg-icons";
+import { faEdit, faPen, faUpload } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { BButton, BLink } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
@@ -21,23 +20,25 @@ import WorkflowPublished from "@/components/Workflow/Published/WorkflowPublished
 import WorkflowInvocationsCount from "@/components/Workflow/WorkflowInvocationsCount.vue";
 import WorkflowRunButton from "@/components/Workflow/WorkflowRunButton.vue";
 
-library.add(faEdit, faEye, faPen, faUpload);
-
 interface Props {
     workflow: any;
     gridView?: boolean;
+    hideRuns?: boolean;
+    filterable?: boolean;
     publishedView?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
     gridView: false,
     publishedView: false,
+    hideRuns: false,
+    filterable: true,
 });
 
 const emit = defineEmits<{
     (e: "tagClick", tag: string): void;
     (e: "refreshList", overlayLoading?: boolean, b?: boolean): void;
-    (e: "update-filter", key: string, value: any): void;
+    (e: "updateFilter", key: string, value: any): void;
 }>();
 
 const userStore = useUserStore();
@@ -95,7 +96,7 @@ async function onImport() {
     Toast.success("Workflow imported successfully");
 }
 
-function onRenameClose(e: any) {
+function onRenameClose() {
     showRename.value = false;
     emit("refreshList", true);
 }
@@ -126,10 +127,14 @@ async function onTagClick(tag: string) {
                 <WorkflowIndicators
                     :workflow="workflow"
                     :published-view="publishedView"
-                    @update-filter="(k, v) => emit('update-filter', k, v)" />
+                    :filterable="props.filterable"
+                    @update-filter="(k, v) => emit('updateFilter', k, v)" />
 
                 <div class="workflow-count-actions">
-                    <WorkflowInvocationsCount v-if="!isAnonymous && !shared" class="mx-1" :workflow="workflow" />
+                    <WorkflowInvocationsCount
+                        v-if="!props.hideRuns && !isAnonymous && !shared"
+                        class="mx-1"
+                        :workflow="workflow" />
 
                     <WorkflowActions
                         :workflow="workflow"
diff --git a/client/src/components/Workflow/List/WorkflowIndicators.vue b/client/src/components/Workflow/List/WorkflowIndicators.vue
index 10f841277c80..2f863a379c6d 100644
--- a/client/src/components/Workflow/List/WorkflowIndicators.vue
+++ b/client/src/components/Workflow/List/WorkflowIndicators.vue
@@ -18,6 +18,7 @@ interface Props {
     workflow: any;
     publishedView: boolean;
     noEditTime?: boolean;
+    filterable?: boolean;
 }
 
 const props = defineProps<Props>();
@@ -30,10 +31,15 @@ const router = useRouter();
 const userStore = useUserStore();
 
 const publishedTitle = computed(() => {
-    if (userStore.matchesCurrentUsername(props.workflow.owner)) {
-        return "Published by you. Click to view all published workflows by you";
+    if (props.workflow.published && !props.publishedView) {
+        return "Published workflow" + (props.filterable ? ". Click to filter published workflows" : "");
+    } else if (userStore.matchesCurrentUsername(props.workflow.owner)) {
+        return "Published by you" + (props.filterable ? ". Click to view all published workflows by you" : "");
     } else {
-        return `Published by '${props.workflow.owner}'. Click to view all published workflows by '${props.workflow.owner}'`;
+        return (
+            `Published by '${props.workflow.owner}'` +
+            (props.filterable ? `. Click to view all published workflows by '${props.workflow.owner}'` : "")
+        );
     }
 });
 
@@ -89,7 +95,7 @@ function onViewUserPublished() {
             v-b-tooltip.noninteractive.hover
             size="sm"
             class="workflow-published-icon inline-icon-button"
-            title="Published workflow. Click to filter published workflows"
+            :title="publishedTitle"
             @click="emit('update-filter', 'published', true)">
             <FontAwesomeIcon :icon="faGlobe" fixed-width />
         </BButton>
diff --git a/client/src/components/Workflow/List/WorkflowList.vue b/client/src/components/Workflow/List/WorkflowList.vue
index 9db7e4865c03..307ebd116404 100644
--- a/client/src/components/Workflow/List/WorkflowList.vue
+++ b/client/src/components/Workflow/List/WorkflowList.vue
@@ -8,7 +8,7 @@ import { computed, onMounted, ref, watch } from "vue";
 import { useRouter } from "vue-router/composables";
 
 import { GalaxyApi } from "@/api";
-import { helpHtml, WorkflowFilters } from "@/components/Workflow/List/WorkflowFilters";
+import { getWorkflowFilters, helpHtml } from "@/components/Workflow/List/workflowFilters";
 import { Toast } from "@/composables/toast";
 import { useUserStore } from "@/stores/userStore";
 import { rethrowSimple } from "@/utils/simple-error";
@@ -77,7 +77,7 @@ const bookmarkButtonTitle = computed(() =>
 );
 
 // Filtering computed refs
-const workflowFilters = computed(() => WorkflowFilters(props.activeList));
+const workflowFilters = computed(() => getWorkflowFilters(props.activeList));
 const rawFilters = computed(() =>
     Object.fromEntries(workflowFilters.value.getFiltersForText(filterText.value, true, false))
 );
@@ -321,7 +321,7 @@ onMounted(() => {
                 :class="view === 'grid' ? 'grid-view' : 'list-view'"
                 @refreshList="load"
                 @tagClick="(tag) => updateFilterValue('tag', `'${tag}'`)"
-                @update-filter="updateFilterValue" />
+                @updateFilter="updateFilterValue" />
 
             <BPagination
                 v-if="!loading && totalWorkflows > limit"
diff --git a/client/src/components/Workflow/List/WorkflowFilters.js b/client/src/components/Workflow/List/workflowFilters.ts
similarity index 98%
rename from client/src/components/Workflow/List/WorkflowFilters.js
rename to client/src/components/Workflow/List/workflowFilters.ts
index a7c1f3f3b3c8..47f523b0a66e 100644
--- a/client/src/components/Workflow/List/WorkflowFilters.js
+++ b/client/src/components/Workflow/List/workflowFilters.ts
@@ -1,4 +1,4 @@
-import Filtering, { contains, equals, expandNameTag, toBool } from "utils/filtering";
+import Filtering, { contains, equals, expandNameTag, toBool } from "@/utils/filtering";
 
 export function helpHtml(activeList = "my") {
     let extra = "";
@@ -71,7 +71,7 @@ export function helpHtml(activeList = "my") {
     return conditionalHelpHtml;
 }
 
-export function WorkflowFilters(activeList = "my") {
+export function getWorkflowFilters(activeList = "my") {
     const commonFilters = {
         name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true },
         n: { handler: contains("n"), menuItem: false },
@@ -82,7 +82,7 @@ export function WorkflowFilters(activeList = "my") {
             menuItem: true,
         },
         t: { type: "MultiTags", handler: contains("t", "t", expandNameTag), menuItem: false },
-    };
+    } as const;
 
     if (activeList === "my") {
         return new Filtering(
diff --git a/client/src/components/Workflow/workflows.services.ts b/client/src/components/Workflow/workflows.services.ts
index 0b5efab9e469..1a39e98d57f0 100644
--- a/client/src/components/Workflow/workflows.services.ts
+++ b/client/src/components/Workflow/workflows.services.ts
@@ -3,7 +3,39 @@ import axios from "axios";
 import { useUserStore } from "@/stores/userStore";
 import { withPrefix } from "@/utils/redirect";
 
-type Workflow = Record<string, never>;
+export type Workflow = Record<string, never>;
+
+interface LoadWorkflowsOptions {
+    sortBy: SortBy;
+    sortDesc: boolean;
+    limit: number;
+    offset: number;
+    filterText: string;
+    showPublished: boolean;
+    skipStepCounts: boolean;
+}
+
+const getWorkflows = fetcher.path("/api/workflows").method("get").create();
+export async function loadWorkflows({
+    sortBy = "update_time",
+    sortDesc = true,
+    limit = 20,
+    offset = 0,
+    filterText = "",
+    showPublished = false,
+    skipStepCounts = true,
+}: LoadWorkflowsOptions): Promise<{ data: Workflow[]; headers: Headers }> {
+    const { data, headers } = await getWorkflows({
+        sort_by: sortBy,
+        sort_desc: sortDesc,
+        limit,
+        offset,
+        search: filterText,
+        show_published: showPublished,
+        skip_step_counts: skipStepCounts,
+    });
+    return { data, headers };
+}
 
 export async function updateWorkflow(id: string, changes: object): Promise<Workflow> {
     const { data } = await axios.put(withPrefix(`/api/workflows/${id}`), changes);
diff --git a/client/src/utils/filtering.ts b/client/src/utils/filtering.ts
index 13030445e978..4b25ad56bec4 100644
--- a/client/src/utils/filtering.ts
+++ b/client/src/utils/filtering.ts
@@ -525,13 +525,16 @@ export default class Filtering<T> {
             if (this.validFilters[key]?.type === "MultiTags" && Array.isArray(value)) {
                 const validValues = value
                     .map((v) => this.getConvertedValue(key, v, backendFormatted))
-                    .filter((v) => v !== undefined);
+                    .filter((v) => v !== undefined) as T[];
+
                 if (validValues.length > 0) {
                     validFilters[key] = validValues as T;
                 }
+
                 const invalidValues = value.filter(
-                    (v) => !validValues.includes(this.getConvertedValue(key, v, backendFormatted))
+                    (v) => !validValues.includes(this.getConvertedValue(key, v, backendFormatted) as T)
                 );
+
                 if (invalidValues.length > 0) {
                     invalidFilters[key] = invalidValues as T;
                 }

From 2b582862c00d960aa2b83a4f7f8b20cff1d2d047 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 29 Aug 2024 12:05:12 +0200
Subject: [PATCH 013/131] improve performance of workflow card make workflow
 card mostly stateless move modals outside of workflow card

---
 .../Panels/Buttons/FavoritesButton.vue        |   7 +-
 .../components/Workflow/List/WorkflowCard.vue |  58 ++------
 .../Workflow/List/WorkflowCardList.vue        | 128 ++++++++++++++++++
 .../components/Workflow/List/WorkflowList.vue |  40 +-----
 4 files changed, 146 insertions(+), 87 deletions(-)
 create mode 100644 client/src/components/Workflow/List/WorkflowCardList.vue

diff --git a/client/src/components/Panels/Buttons/FavoritesButton.vue b/client/src/components/Panels/Buttons/FavoritesButton.vue
index 190e8d23fdb3..79941f3395f7 100644
--- a/client/src/components/Panels/Buttons/FavoritesButton.vue
+++ b/client/src/components/Panels/Buttons/FavoritesButton.vue
@@ -14,13 +14,12 @@ library.add(faStar, faRegStar);
 
 interface Props {
     value?: boolean;
-    modelValue?: boolean;
     query?: string;
 }
 
 const props = defineProps<Props>();
 
-const currentValue = computed(() => props.value ?? props.modelValue ?? false);
+const currentValue = computed(() => props.value ?? false);
 const toggle = ref(false);
 
 watchImmediate(
@@ -30,7 +29,7 @@ watchImmediate(
 
 const emit = defineEmits<{
     (e: "change", toggled: boolean): void;
-    (e: "update:modelValue", toggled: boolean): void;
+    (e: "input", toggled: boolean): void;
 }>();
 
 const { isAnonymous } = storeToRefs(useUserStore());
@@ -58,7 +57,7 @@ watch(
 
 function toggleFavorites() {
     toggle.value = !toggle.value;
-    emit("update:modelValue", toggle.value);
+    emit("input", toggle.value);
     emit("change", toggle.value);
 }
 </script>
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 39cc9748dc6a..4913f938a2bb 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -3,7 +3,7 @@ import { faEdit, faPen, faUpload } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { BButton, BLink } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
-import { computed, ref } from "vue";
+import { computed } from "vue";
 
 import { copyWorkflow, updateWorkflow } from "@/components/Workflow/workflows.services";
 import { Toast } from "@/composables/toast";
@@ -15,8 +15,6 @@ import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
 import WorkflowActions from "@/components/Workflow/List/WorkflowActions.vue";
 import WorkflowActionsExtend from "@/components/Workflow/List/WorkflowActionsExtend.vue";
 import WorkflowIndicators from "@/components/Workflow/List/WorkflowIndicators.vue";
-import WorkflowRename from "@/components/Workflow/List/WorkflowRename.vue";
-import WorkflowPublished from "@/components/Workflow/Published/WorkflowPublished.vue";
 import WorkflowInvocationsCount from "@/components/Workflow/WorkflowInvocationsCount.vue";
 import WorkflowRunButton from "@/components/Workflow/WorkflowRunButton.vue";
 
@@ -37,17 +35,16 @@ const props = withDefaults(defineProps<Props>(), {
 
 const emit = defineEmits<{
     (e: "tagClick", tag: string): void;
-    (e: "refreshList", overlayLoading?: boolean, b?: boolean): void;
+    (e: "refreshList", overlayLoading?: boolean, silent?: boolean): void;
     (e: "updateFilter", key: string, value: any): void;
+    (e: "rename", id: string, name: string): void;
+    (e: "preview", id: string): void;
 }>();
 
 const userStore = useUserStore();
 
 const { isAnonymous } = storeToRefs(userStore);
 
-const showRename = ref(false);
-const showPreview = ref(false);
-
 const workflow = computed(() => props.workflow);
 
 const shared = computed(() => {
@@ -96,15 +93,6 @@ async function onImport() {
     Toast.success("Workflow imported successfully");
 }
 
-function onRenameClose() {
-    showRename.value = false;
-    emit("refreshList", true);
-}
-
-function toggleShowPreview(val: boolean = false) {
-    showPreview.value = val;
-}
-
 async function onTagsUpdate(tags: string[]) {
     workflow.value.tags = tags;
     await updateWorkflow(workflow.value.id, { tags });
@@ -139,8 +127,7 @@ async function onTagClick(tag: string) {
                     <WorkflowActions
                         :workflow="workflow"
                         :published="publishedView"
-                        @refreshList="emit('refreshList', true)"
-                        @toggleShowPreview="toggleShowPreview" />
+                        @refreshList="emit('refreshList', true)" />
                 </div>
 
                 <span class="workflow-name font-weight-bold">
@@ -148,9 +135,9 @@ async function onTagClick(tag: string) {
                         v-b-tooltip.hover.noninteractive
                         class="workflow-name-preview"
                         title="Preview Workflow"
-                        @click.stop.prevent="toggleShowPreview(true)"
-                        >{{ workflow.name }}</BLink
-                    >
+                        @click.stop.prevent="emit('preview', props.workflow.id)">
+                        {{ workflow.name }}
+                    </BLink>
                     <BButton
                         v-if="!shared && !workflow.deleted"
                         v-b-tooltip.hover.noninteractive
@@ -159,7 +146,7 @@ async function onTagClick(tag: string) {
                         variant="link"
                         size="sm"
                         title="Rename"
-                        @click="showRename = !showRename">
+                        @click="emit('rename', props.workflow.id, props.workflow.name)">
                         <FontAwesomeIcon :icon="faPen" fixed-width />
                     </BButton>
                 </span>
@@ -221,37 +208,10 @@ async function onTagClick(tag: string) {
                     </div>
                 </div>
             </div>
-
-            <WorkflowRename
-                v-if="!isAnonymous && !shared && !workflow.deleted"
-                :id="workflow.id"
-                :show="showRename"
-                :name="workflow.name"
-                @close="onRenameClose" />
-
-            <BModal
-                v-model="showPreview"
-                ok-only
-                size="xl"
-                hide-header
-                dialog-class="workflow-card-preview-modal w-auto"
-                centered>
-                <WorkflowPublished v-if="showPreview" :id="workflow.id" quick-view />
-            </BModal>
         </div>
     </div>
 </template>
 
-<style lang="scss">
-.workflow-card-preview-modal {
-    max-width: min(1400px, calc(100% - 200px));
-
-    .modal-content {
-        height: min(800px, calc(100vh - 80px));
-    }
-}
-</style>
-
 <style scoped lang="scss">
 @import "theme/blue.scss";
 @import "breakpoints.scss";
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
new file mode 100644
index 000000000000..74e913d444f8
--- /dev/null
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -0,0 +1,128 @@
+<script setup lang="ts">
+import { BModal } from "bootstrap-vue";
+import { reactive, ref } from "vue";
+
+import type { Workflow } from "@/components/Workflow/workflows.services";
+
+import WorkflowCard from "./WorkflowCard.vue";
+import WorkflowRename from "./WorkflowRename.vue";
+import WorkflowPublished from "@/components/Workflow/Published/WorkflowPublished.vue";
+
+interface Props {
+    workflows: Workflow[];
+    gridView?: boolean;
+    hideRuns?: boolean;
+    filterable?: boolean;
+    publishedView?: boolean;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+    (e: "tagClick", tag: string): void;
+    (e: "refreshList", overlayLoading?: boolean, silent?: boolean): void;
+    (e: "updateFilter", key: string, value: any): void;
+}>();
+
+const modalOptions = reactive({
+    rename: {
+        id: "",
+        name: "",
+    },
+    preview: {
+        id: "",
+    },
+});
+
+const showRename = ref(false);
+
+function onRenameClose() {
+    showRename.value = false;
+    emit("refreshList", true);
+}
+
+function onRename(id: string, name: string) {
+    modalOptions.rename.id = id;
+    modalOptions.rename.name = name;
+    showRename.value = true;
+}
+
+const showPreview = ref(false);
+
+function onPreview(id: string) {
+    modalOptions.preview.id = id;
+    showPreview.value = true;
+}
+</script>
+
+<template>
+    <div class="workflow-card-list" :class="{ grid: props.gridView }">
+        <WorkflowCard
+            v-for="workflow in props.workflows"
+            :key="workflow.id"
+            :workflow="workflow"
+            :grid-view="props.gridView"
+            :hide-runs="props.hideRuns"
+            :filterable="props.filterable"
+            :published-view="props.publishedView"
+            class="workflow-card"
+            @tagClick="(...args) => emit('tagClick', ...args)"
+            @refreshList="(...args) => emit('refreshList', ...args)"
+            @updateFilter="(...args) => emit('updateFilter', ...args)"
+            @rename="onRename"
+            @preview="onPreview">
+        </WorkflowCard>
+
+        <WorkflowRename
+            :id="modalOptions.rename.id"
+            :show="showRename"
+            :name="modalOptions.rename.name"
+            @close="onRenameClose" />
+
+        <BModal
+            v-model="showPreview"
+            ok-only
+            size="xl"
+            hide-header
+            dialog-class="workflow-card-preview-modal w-auto"
+            centered>
+            <WorkflowPublished v-if="showPreview" :id="modalOptions.preview.id" quick-view />
+        </BModal>
+    </div>
+</template>
+
+<style lang="scss">
+.workflow-card-preview-modal {
+    max-width: min(1400px, calc(100% - 200px));
+
+    .modal-content {
+        height: min(800px, calc(100vh - 80px));
+    }
+}
+</style>
+
+<style scoped lang="scss">
+@import "breakpoints.scss";
+
+.workflow-card-list {
+    container: card-list / inline-size;
+    display: flex;
+    flex-wrap: wrap;
+
+    .workflow-card {
+        width: 100%;
+    }
+
+    &.grid .workflow-card {
+        width: calc(100% / 3);
+
+        @container card-list (max-width: #{$breakpoint-xl}) {
+            width: calc(100% / 2);
+        }
+
+        @container card-list (max-width: #{$breakpoint-sm}) {
+            width: 100%;
+        }
+    }
+}
+</style>
diff --git a/client/src/components/Workflow/List/WorkflowList.vue b/client/src/components/Workflow/List/WorkflowList.vue
index 307ebd116404..06def93b18da 100644
--- a/client/src/components/Workflow/List/WorkflowList.vue
+++ b/client/src/components/Workflow/List/WorkflowList.vue
@@ -13,12 +13,12 @@ import { Toast } from "@/composables/toast";
 import { useUserStore } from "@/stores/userStore";
 import { rethrowSimple } from "@/utils/simple-error";
 
+import WorkflowCardList from "./WorkflowCardList.vue";
 import FilterMenu from "@/components/Common/FilterMenu.vue";
 import Heading from "@/components/Common/Heading.vue";
 import ListHeader from "@/components/Common/ListHeader.vue";
 import LoginRequired from "@/components/Common/LoginRequired.vue";
 import LoadingSpan from "@/components/LoadingSpan.vue";
-import WorkflowCard from "@/components/Workflow/List/WorkflowCard.vue";
 import WorkflowListActions from "@/components/Workflow/List/WorkflowListActions.vue";
 
 library.add(faStar, faTrash);
@@ -305,20 +305,11 @@ onMounted(() => {
             </BAlert>
         </span>
 
-        <BOverlay
-            v-else
-            id="workflow-cards"
-            :show="overlay"
-            rounded="sm"
-            class="cards-list mt-2"
-            :class="view === 'grid' ? 'd-flex flex-wrap' : ''">
-            <WorkflowCard
-                v-for="w in workflowsLoaded"
-                :key="w.id"
-                :workflow="w"
+        <BOverlay v-else id="workflow-cards" :show="overlay" rounded="sm" class="cards-list mt-2">
+            <WorkflowCardList
+                :workflows="workflowsLoaded"
                 :published-view="published"
                 :grid-view="view === 'grid'"
-                :class="view === 'grid' ? 'grid-view' : 'list-view'"
                 @refreshList="load"
                 @tagClick="(tag) => updateFilterValue('tag', `'${tag}'`)"
                 @updateFilter="updateFilterValue" />
@@ -358,32 +349,13 @@ onMounted(() => {
     }
 
     .cards-list {
-        container: card-list / inline-size;
         scroll-behavior: smooth;
         min-height: 150px;
+        display: flex;
+        flex-direction: column;
 
         overflow-y: auto;
         overflow-x: hidden;
-
-        .list-view {
-            width: 100%;
-        }
-
-        .grid-view {
-            width: calc(100% / 3);
-        }
-
-        @container card-list (max-width: #{$breakpoint-xl}) {
-            .grid-view {
-                width: calc(100% / 2);
-            }
-        }
-
-        @container card-list (max-width: #{$breakpoint-sm}) {
-            .grid-view {
-                width: 100%;
-            }
-        }
     }
 }
 </style>

From 1a55ad87de9a03010c437ecb8205a2b877635cb8 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:57:04 +0200
Subject: [PATCH 014/131] add workflows panel

---
 .../src/components/Panels/ActivityPanel.vue   |   1 +
 .../src/components/Panels/WorkflowPanel.vue   | 145 +++++++++++
 .../src/components/Workflow/Editor/Index.vue  |   5 +-
 .../Workflow/List/WorkflowActions.vue         | 132 +++++-----
 .../Workflow/List/WorkflowActionsExtend.vue   | 236 ++++++++++++------
 .../components/Workflow/List/WorkflowCard.vue | 125 ++--------
 .../Workflow/List/WorkflowCardList.vue        |   2 +
 .../Workflow/List/WorkflowIndicators.vue      |  52 ++--
 .../Workflow/List/useWorkflowActions.ts       | 110 ++++++++
 .../Workflow/WorkflowInvocationsCount.vue     |   4 +-
 .../components/Workflow/workflows.services.ts |  36 ++-
 11 files changed, 576 insertions(+), 272 deletions(-)
 create mode 100644 client/src/components/Panels/WorkflowPanel.vue
 create mode 100644 client/src/components/Workflow/List/useWorkflowActions.ts

diff --git a/client/src/components/Panels/ActivityPanel.vue b/client/src/components/Panels/ActivityPanel.vue
index df4d58a6de64..b0349150a92b 100644
--- a/client/src/components/Panels/ActivityPanel.vue
+++ b/client/src/components/Panels/ActivityPanel.vue
@@ -84,6 +84,7 @@ const hasGoToAll = computed(
         flex-direction: column;
         flex-grow: 1;
         overflow-y: hidden;
+        position: relative;
         button:first-child {
             background: none;
             border: none;
diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
new file mode 100644
index 000000000000..ec20955e0745
--- /dev/null
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -0,0 +1,145 @@
+<script setup lang="ts">
+import { useMemoize, watchImmediate } from "@vueuse/core";
+import { computed, ref, watch } from "vue";
+
+import { loadWorkflows, type Workflow } from "@/components/Workflow/workflows.services";
+import { useAnimationFrameScroll } from "@/composables/sensors/animationFrameScroll";
+import { useToast } from "@/composables/toast";
+
+import ActivityPanel from "./ActivityPanel.vue";
+import DelayedInput from "@/components/Common/DelayedInput.vue";
+import ScrollToTopButton from "@/components/ToolsList/ScrollToTopButton.vue";
+import WorkflowCardList from "@/components/Workflow/List/WorkflowCardList.vue";
+
+const scrollable = ref<HTMLDivElement | null>(null);
+const { arrived, scrollTop } = useAnimationFrameScroll(scrollable);
+
+const loading = ref(false);
+const totalWorkflowsCount = ref(Infinity);
+
+const allLoaded = computed(() => totalWorkflowsCount.value <= workflows.value.length);
+
+const filterText = ref("");
+
+const workflows = ref<Workflow[]>([]);
+
+const loadWorkflowsOptions = {
+    sortBy: "update_time",
+    sortDesc: true,
+    limit: 20,
+    showPublished: true,
+    skipStepCounts: false,
+} as const;
+
+const { error } = useToast();
+const getWorkflows = useMemoize(async (filterText: string, offset: number) => {
+    const { data, totalMatches } = await loadWorkflows({
+        ...loadWorkflowsOptions,
+        offset,
+        filterText,
+    });
+
+    return { data, totalMatches };
+});
+
+let fetchKey = "";
+let lastFetchKey = "";
+
+async function load() {
+    const isCurrentFetch = () => fetchKey.trim().toLowerCase() === lastFetchKey.trim().toLowerCase();
+
+    if (isCurrentFetch() && (loading.value || allLoaded.value)) {
+        return;
+    }
+
+    lastFetchKey = fetchKey;
+
+    loading.value = true;
+
+    try {
+        const { data, totalMatches } = await getWorkflows(filterText.value, workflows.value.length);
+
+        if (isCurrentFetch()) {
+            totalWorkflowsCount.value = totalMatches;
+
+            const workflowIds = new Set(workflows.value.map((w) => w.id));
+            const newWorkflows = data.filter((w) => !workflowIds.has(w.id));
+
+            workflows.value.push(...newWorkflows);
+        }
+    } catch (e) {
+        if (isCurrentFetch()) {
+            error(`Failed to load workflows: ${e}`);
+        }
+    } finally {
+        if (isCurrentFetch()) {
+            loading.value = false;
+        }
+    }
+}
+
+watchImmediate(
+    () => filterText.value,
+    () => {
+        workflows.value = [];
+        fetchKey = filterText.value;
+        load();
+    }
+);
+
+watch(
+    () => arrived.bottom,
+    () => {
+        if (arrived.bottom) {
+            load();
+        }
+    }
+);
+
+function scrollToTop() {
+    scrollable.value?.scrollTo({ top: 0, behavior: "smooth" });
+}
+</script>
+
+<template>
+    <ActivityPanel title="Workflows">
+        <!-- favorites Button disabled until workflows api is fixed -->
+        <!--template v-slot:header-buttons>
+            <FavoritesButton v-model="showFavorites"></FavoritesButton>
+        </template-->
+
+        <DelayedInput
+            v-model="filterText"
+            placeholder="search workflows"
+            :delay="800"
+            :loading="loading"></DelayedInput>
+
+        <div ref="scrollable" class="workflow-scroll-list mt-2">
+            <WorkflowCardList :hide-runs="true" :workflows="workflows" :filterable="false" editor-view />
+
+            <div v-if="allLoaded || filterText !== ''" class="list-end">
+                <span v-if="workflows.length == 1"> - 1 workflow loaded - </span>
+                <span v-else-if="workflows.length > 1"> - All {{ workflows.length }} workflows loaded - </span>
+            </div>
+            <div v-else-if="loading" class="list-end">- loading -</div>
+        </div>
+
+        <ScrollToTopButton :offset="scrollTop" @click="scrollToTop" />
+    </ActivityPanel>
+</template>
+
+<style scoped lang="scss">
+@import "theme/blue.scss";
+
+.workflow-scroll-list {
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+}
+
+.list-end {
+    align-self: center;
+    color: $text-light;
+    margin: 0.5rem;
+}
+</style>
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 25001ca8c418..0dddc75d8005 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -57,7 +57,8 @@
                     @onUnhighlight="onUnhighlight"
                     @onRefactor="onAttemptRefactor"
                     @onScrollTo="onScrollTo" />
-                <UndoRedoStack v-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
+                <UndoRedoStack v-else-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
+                <WorkflowPanel v-else-if="isActiveSideBar('workflow-editor-workflows')"></WorkflowPanel>
             </template>
         </ActivityBar>
         <div id="center" class="workflow-center">
@@ -231,6 +232,7 @@ import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
 import FlexPanel from "@/components/Panels/FlexPanel.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
+import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue";
 import UndoRedoStack from "@/components/UndoRedo/UndoRedoStack.vue";
 import FormDefault from "@/components/Workflow/Editor/Forms/FormDefault.vue";
 import FormTool from "@/components/Workflow/Editor/Forms/FormTool.vue";
@@ -255,6 +257,7 @@ export default {
         WorkflowGraph,
         FontAwesomeIcon,
         UndoRedoStack,
+        WorkflowPanel,
     },
     props: {
         workflowId: {
diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue
index 91c5524c1ac4..a80fbcab2f02 100644
--- a/client/src/components/Workflow/List/WorkflowActions.vue
+++ b/client/src/components/Workflow/List/WorkflowActions.vue
@@ -1,48 +1,48 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
 import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
 import {
     faCaretDown,
+    faCopy,
     faExternalLinkAlt,
-    faEye,
     faFileExport,
+    faLink,
+    faPlay,
     faSpinner,
     faStar,
     faTrash,
 } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { BButton, BButtonGroup } from "bootstrap-vue";
+import { BButton, BButtonGroup, BDropdown, BDropdownItem } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
-import { computed, ref } from "vue";
+import { computed } from "vue";
 
-import { getGalaxyInstance } from "@/app";
-import { deleteWorkflow, updateWorkflow } from "@/components/Workflow/workflows.services";
-import { useConfirmDialog } from "@/composables/confirmDialog";
-import { Toast } from "@/composables/toast";
 import { useUserStore } from "@/stores/userStore";
 
-library.add(farStar, faCaretDown, faExternalLinkAlt, faEye, faFileExport, faSpinner, faStar, faTrash);
+import { useWorkflowActions } from "./useWorkflowActions";
 
 interface Props {
     workflow: any;
     published?: boolean;
+    editor?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
     published: false,
+    editor: false,
 });
 
 const emit = defineEmits<{
-    (e: "refreshList", a?: boolean): void;
+    (e: "refreshList", overlayLoading?: boolean): void;
 }>();
 
+const { bookmarkLoading, deleteWorkflow, toggleBookmark, copyPublicLink, copyWorkflow } = useWorkflowActions(
+    computed(() => props.workflow),
+    () => emit("refreshList", true)
+);
+
 const userStore = useUserStore();
 const { isAnonymous } = storeToRefs(userStore);
 
-const { confirm } = useConfirmDialog();
-
-const bookmarkLoading = ref(false);
-
 const shared = computed(() => {
     return !userStore.matchesCurrentUsername(props.workflow.owner);
 });
@@ -57,48 +57,12 @@ const sourceType = computed(() => {
     }
 });
 
-async function onToggleBookmark(checked: boolean) {
-    try {
-        bookmarkLoading.value = true;
-
-        await updateWorkflow(props.workflow.id, {
-            show_in_tool_panel: checked,
-        });
-
-        Toast.info(`Workflow ${checked ? "added to" : "removed from"} bookmarks`);
-
-        if (checked) {
-            getGalaxyInstance().config.stored_workflow_menu_entries.push({
-                id: props.workflow.id,
-                name: props.workflow.name,
-            });
-        } else {
-            const indexToRemove = getGalaxyInstance().config.stored_workflow_menu_entries.findIndex(
-                (w: any) => w.id === props.workflow.id
-            );
-            getGalaxyInstance().config.stored_workflow_menu_entries.splice(indexToRemove, 1);
-        }
-    } catch (error) {
-        Toast.error("Failed to update workflow bookmark status");
-    } finally {
-        emit("refreshList", true);
-        bookmarkLoading.value = false;
-    }
-}
-
-async function onDelete() {
-    const confirmed = await confirm("Are you sure you want to delete this workflow?", {
-        title: "Delete workflow",
-        okTitle: "Delete",
-        okVariant: "danger",
-    });
-
-    if (confirmed) {
-        await deleteWorkflow(props.workflow.id);
-        emit("refreshList", true);
-        Toast.info("Workflow deleted");
-    }
-}
+const runPath = computed(
+    () =>
+        `/workflows/run?id=${props.workflow.id}${
+            props.workflow.version !== undefined ? `&version=${props.workflow.version}` : ""
+        }`
+);
 </script>
 
 <template>
@@ -112,7 +76,7 @@ async function onDelete() {
                 title="Add to bookmarks"
                 tooltip="Add to bookmarks. This workflow will appear in the left tool panel."
                 size="sm"
-                @click="onToggleBookmark(true)">
+                @click="toggleBookmark">
                 <FontAwesomeIcon v-if="!bookmarkLoading" :icon="farStar" fixed-width />
                 <FontAwesomeIcon v-else :icon="faSpinner" spin fixed-width />
             </BButton>
@@ -123,7 +87,7 @@ async function onDelete() {
                 variant="link"
                 title="Remove bookmark"
                 size="sm"
-                @click="onToggleBookmark(false)">
+                @click="toggleBookmark">
                 <FontAwesomeIcon v-if="!bookmarkLoading" :icon="faStar" fixed-width />
                 <FontAwesomeIcon v-else :icon="faSpinner" spin fixed-width />
             </BButton>
@@ -141,13 +105,63 @@ async function onDelete() {
                     <FontAwesomeIcon :icon="faCaretDown" fixed-width />
                 </template>
 
+                <template v-if="props.editor">
+                    <BDropdownItem :to="runPath" title="Run workflow" size="sm" variant="link">
+                        <FontAwesomeIcon :icon="faPlay" fixed-width />
+                        Run
+                    </BDropdownItem>
+
+                    <BDropdownItem
+                        v-if="props.workflow.published"
+                        size="sm"
+                        title="Copy link to workflow"
+                        variant="link"
+                        @click="copyPublicLink">
+                        <FontAwesomeIcon :icon="faLink" fixed-width />
+                        Link to Workflow
+                    </BDropdownItem>
+
+                    <BDropdownItem
+                        v-if="!isAnonymous && !shared"
+                        size="sm"
+                        title="Copy workflow"
+                        variant="link"
+                        @click="copyWorkflow">
+                        <FontAwesomeIcon :icon="faCopy" fixed-width />
+                        Copy
+                    </BDropdownItem>
+
+                    <!--BButton
+                        id="workflow-download-button"
+                        v-b-tooltip.hover.noninteractive
+                        size="sm"
+                        title="Download workflow in .ga format"
+                        variant="outline-primary"
+                        :href="downloadUrl">
+                        <FontAwesomeIcon :icon="faDownload" fixed-width />
+                        <span class="compact-view">Download</span>
+                    </BButton>
+
+                    <BButton
+                        v-if="!isAnonymous && !shared"
+                        id="workflow-share-button"
+                        v-b-tooltip.hover.noninteractive
+                        size="sm"
+                        title="Share"
+                        variant="outline-primary"
+                        :to="`/workflows/sharing?id=${workflow.id}`">
+                        <FontAwesomeIcon :icon="faShareAlt" fixed-width />
+                        <span class="compact-view">Share</span>
+                    </BButton-->
+                </template>
+
                 <BDropdownItem
                     v-if="!isAnonymous && !shared && !props.workflow.deleted"
                     class="workflow-delete-button"
                     title="Delete workflow"
                     size="sm"
                     variant="link"
-                    @click="onDelete">
+                    @click="deleteWorkflow">
                     <FontAwesomeIcon :icon="faTrash" fixed-width />
                     <span>Delete</span>
                 </BDropdownItem>
diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
index ae5878a44e7f..f896fe89ceff 100644
--- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue
+++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
@@ -1,56 +1,54 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faCopy, faDownload, faLink, faShareAlt, faTrashRestore } from "@fortawesome/free-solid-svg-icons";
+import {
+    faCopy,
+    faDownload,
+    faEdit,
+    faLink,
+    faPlusSquare,
+    faShareAlt,
+    faTrashRestore,
+    faUpload,
+} from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { BButton } from "bootstrap-vue";
+import { BButton, BButtonGroup } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
-import { copyWorkflow, undeleteWorkflow } from "@/components/Workflow/workflows.services";
+import { undeleteWorkflow } from "@/components/Workflow/workflows.services";
 import { useConfirmDialog } from "@/composables/confirmDialog";
 import { Toast } from "@/composables/toast";
 import { useUserStore } from "@/stores/userStore";
-import { copy } from "@/utils/clipboard";
-import { withPrefix } from "@/utils/redirect";
-import { getFullAppUrl } from "@/utils/utils";
 
-library.add(faCopy, faDownload, faLink, faShareAlt, faTrashRestore);
+import { useWorkflowActions } from "./useWorkflowActions";
+
+import AsyncButton from "@/components/Common/AsyncButton.vue";
+import WorkflowRunButton from "@/components/Workflow/WorkflowRunButton.vue";
 
 interface Props {
     workflow: any;
     published?: boolean;
+    editor?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
     published: false,
+    editor: false,
 });
 
 const emit = defineEmits<{
     (e: "refreshList", overlayLoading?: boolean): void;
+    (e: "insert"): void;
+    (e: "copySteps"): void;
 }>();
 
 const userStore = useUserStore();
 const { confirm } = useConfirmDialog();
 const { isAnonymous } = storeToRefs(useUserStore());
 
-const downloadUrl = computed(() => {
-    return withPrefix(`/api/workflows/${props.workflow.id}/download?format=json-download`);
-});
-
 const shared = computed(() => {
     return !userStore.matchesCurrentUsername(props.workflow.owner);
 });
 
-async function onCopy() {
-    const confirmed = await confirm("Are you sure you want to make a copy of this workflow?", "Copy workflow");
-
-    if (confirmed) {
-        await copyWorkflow(props.workflow.id, props.workflow.owner);
-        emit("refreshList", true);
-        Toast.success("Workflow copied");
-    }
-}
-
 async function onRestore() {
     const confirmed = await confirm("Are you sure you want to restore this workflow?", "Restore workflow");
 
@@ -61,94 +59,172 @@ async function onRestore() {
     }
 }
 
-const relativeLink = computed(() => {
-    return `/published/workflow?id=${props.workflow.id}`;
+const editButtonTitle = computed(() => {
+    if (isAnonymous.value) {
+        return "Log in to edit Workflow";
+    } else {
+        if (props.workflow.deleted) {
+            return "You cannot edit a deleted workflow. Restore it first.";
+        } else {
+            return "Edit Workflow";
+        }
+    }
 });
-
-const fullLink = computed(() => {
-    return getFullAppUrl(relativeLink.value.substring(1));
+const importedButtonTitle = computed(() => {
+    if (isAnonymous.value) {
+        return "Log in to import workflow";
+    } else {
+        return "Import this workflow to edit";
+    }
+});
+const runButtonTitle = computed(() => {
+    if (isAnonymous.value) {
+        return "Log in to run workflow";
+    } else {
+        if (props.workflow.deleted) {
+            return "You cannot run a deleted workflow. Restore it first.";
+        } else {
+            return "Run workflow";
+        }
+    }
 });
 
-function onCopyPublicLink() {
-    copy(fullLink.value);
-    Toast.success("Link to workflow copied");
-}
+const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflowActions(
+    computed(() => props.workflow),
+    () => emit("refreshList", true)
+);
 </script>
 
 <template>
-    <div class="workflow-actions-extend flex-gapx-1">
+    <div class="workflow-card-actions flex-gapx-1">
         <BButtonGroup>
-            <BButton
-                v-if="workflow.published && !workflow.deleted"
-                id="workflow-copy-public-button"
-                v-b-tooltip.hover.noninteractive
-                size="sm"
-                title="Copy link to workflow"
-                variant="outline-primary"
-                @click="onCopyPublicLink">
-                <FontAwesomeIcon :icon="faLink" fixed-width />
-                <span class="compact-view">Link to Workflow</span>
-            </BButton>
+            <template v-if="!props.editor && !workflow.deleted">
+                <BButton
+                    v-if="workflow.published"
+                    id="workflow-copy-public-button"
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Copy link to workflow"
+                    variant="outline-primary"
+                    @click="copyPublicLink">
+                    <FontAwesomeIcon :icon="faLink" fixed-width />
+                    <span class="compact-view">Link to Workflow</span>
+                </BButton>
+
+                <BButton
+                    v-if="!isAnonymous && !shared"
+                    id="workflow-copy-button"
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Copy"
+                    variant="outline-primary"
+                    @click="copyWorkflow">
+                    <FontAwesomeIcon :icon="faCopy" fixed-width />
+                    <span class="compact-view">Copy</span>
+                </BButton>
+
+                <BButton
+                    id="workflow-download-button"
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Download workflow in .ga format"
+                    variant="outline-primary"
+                    :href="downloadUrl">
+                    <FontAwesomeIcon :icon="faDownload" fixed-width />
+                    <span class="compact-view">Download</span>
+                </BButton>
+
+                <BButton
+                    v-if="!isAnonymous && !shared"
+                    id="workflow-share-button"
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Share"
+                    variant="outline-primary"
+                    :to="`/workflows/sharing?id=${workflow.id}`">
+                    <FontAwesomeIcon :icon="faShareAlt" fixed-width />
+                    <span class="compact-view">Share</span>
+                </BButton>
+            </template>
 
             <BButton
-                v-if="!isAnonymous && !shared && !workflow.deleted"
-                id="workflow-copy-button"
-                v-b-tooltip.hover.noninteractive
-                size="sm"
-                title="Copy"
-                variant="outline-primary"
-                @click="onCopy">
-                <FontAwesomeIcon :icon="faCopy" fixed-width />
-                <span class="compact-view">Copy</span>
-            </BButton>
-
-            <BButton
-                v-if="!workflow.deleted"
-                id="workflow-download-button"
+                v-if="workflow.deleted"
+                id="restore-button"
                 v-b-tooltip.hover.noninteractive
                 size="sm"
-                title="Download workflow in .ga format"
+                title="Restore"
                 variant="outline-primary"
-                :href="downloadUrl">
-                <FontAwesomeIcon :icon="faDownload" fixed-width />
-                <span class="compact-view">Download</span>
+                @click="onRestore">
+                <FontAwesomeIcon :icon="faTrashRestore" fixed-width />
+                <span class="compact-view">Restore</span>
             </BButton>
+        </BButtonGroup>
 
+        <div>
             <BButton
-                v-if="!isAnonymous && !shared && !workflow.deleted"
-                id="workflow-share-button"
+                v-if="!isAnonymous && !shared"
                 v-b-tooltip.hover.noninteractive
+                :disabled="workflow.deleted"
                 size="sm"
-                title="Share"
+                class="workflow-edit-button"
+                :title="editButtonTitle"
                 variant="outline-primary"
-                :to="`/workflows/sharing?id=${workflow.id}`">
-                <FontAwesomeIcon :icon="faShareAlt" fixed-width />
-                <span class="compact-view">Share</span>
+                :to="`/workflows/edit?id=${workflow.id}`">
+                <FontAwesomeIcon :icon="faEdit" fixed-width />
+                Edit
             </BButton>
 
-            <BButton
-                v-if="workflow.deleted"
-                id="restore-button"
+            <AsyncButton
+                v-else
                 v-b-tooltip.hover.noninteractive
                 size="sm"
-                title="Restore"
+                :disabled="isAnonymous"
+                :title="importedButtonTitle"
+                :icon="faUpload"
                 variant="outline-primary"
-                @click="onRestore">
-                <FontAwesomeIcon :icon="faTrashRestore" fixed-width />
-                <span class="compact-view">Restore</span>
-            </BButton>
-        </BButtonGroup>
+                :action="importWorkflow">
+                Import
+            </AsyncButton>
+
+            <WorkflowRunButton
+                v-if="!props.editor"
+                :id="workflow.id"
+                :disabled="isAnonymous || workflow.deleted"
+                :title="runButtonTitle" />
+
+            <BButtonGroup v-if="props.editor && !workflow.deleted">
+                <BButton
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Copy steps into workflow"
+                    variant="outline-primary"
+                    @click="emit('copySteps')">
+                    <FontAwesomeIcon :icon="faCopy" fixed-width />
+                </BButton>
+
+                <BButton
+                    v-b-tooltip.hover.noninteractive
+                    size="sm"
+                    title="Insert as sub-workflow"
+                    variant="primary"
+                    @click="emit('insert')">
+                    <FontAwesomeIcon :icon="faPlusSquare" fixed-width />
+                    <span> Insert </span>
+                </BButton>
+            </BButtonGroup>
+        </div>
     </div>
 </template>
 
 <style scoped lang="scss">
 @import "breakpoints.scss";
 
-.workflow-actions-extend {
+.workflow-card-actions {
     display: flex;
-    align-items: baseline;
-    flex-wrap: wrap;
-    justify-content: flex-end;
+    gap: 0.25rem;
+    margin-top: 0.25rem;
+    align-items: center;
+    justify-content: end;
 
     @container (max-width: #{$breakpoint-md}) {
         .compact-view {
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 4913f938a2bb..953570a7019c 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -1,22 +1,19 @@
 <script setup lang="ts">
-import { faEdit, faPen, faUpload } from "@fortawesome/free-solid-svg-icons";
+import { faPen } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { BButton, BLink } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
-import { copyWorkflow, updateWorkflow } from "@/components/Workflow/workflows.services";
-import { Toast } from "@/composables/toast";
+import { updateWorkflow } from "@/components/Workflow/workflows.services";
 import { useUserStore } from "@/stores/userStore";
 
-import AsyncButton from "@/components/Common/AsyncButton.vue";
 import TextSummary from "@/components/Common/TextSummary.vue";
 import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
 import WorkflowActions from "@/components/Workflow/List/WorkflowActions.vue";
 import WorkflowActionsExtend from "@/components/Workflow/List/WorkflowActionsExtend.vue";
 import WorkflowIndicators from "@/components/Workflow/List/WorkflowIndicators.vue";
 import WorkflowInvocationsCount from "@/components/Workflow/WorkflowInvocationsCount.vue";
-import WorkflowRunButton from "@/components/Workflow/WorkflowRunButton.vue";
 
 interface Props {
     workflow: any;
@@ -24,6 +21,7 @@ interface Props {
     hideRuns?: boolean;
     filterable?: boolean;
     publishedView?: boolean;
+    editorView?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -31,6 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
     publishedView: false,
     hideRuns: false,
     filterable: true,
+    editorView: false,
 });
 
 const emit = defineEmits<{
@@ -58,40 +57,6 @@ const description = computed(() => {
         return null;
     }
 });
-const editButtonTitle = computed(() => {
-    if (isAnonymous.value) {
-        return "Log in to edit Workflow";
-    } else {
-        if (workflow.value.deleted) {
-            return "You cannot edit a deleted workflow. Restore it first.";
-        } else {
-            return "Edit Workflow";
-        }
-    }
-});
-const importedButtonTitle = computed(() => {
-    if (isAnonymous.value) {
-        return "Log in to import workflow";
-    } else {
-        return "Import this workflow to edit";
-    }
-});
-const runButtonTitle = computed(() => {
-    if (isAnonymous.value) {
-        return "Log in to run workflow";
-    } else {
-        if (workflow.value.deleted) {
-            return "You cannot run a deleted workflow. Restore it first.";
-        } else {
-            return "Run workflow";
-        }
-    }
-});
-
-async function onImport() {
-    await copyWorkflow(workflow.value.id, workflow.value.owner);
-    Toast.success("Workflow imported successfully");
-}
 
 async function onTagsUpdate(tags: string[]) {
     workflow.value.tags = tags;
@@ -113,10 +78,11 @@ async function onTagClick(tag: string) {
             }">
             <div class="workflow-card-header">
                 <WorkflowIndicators
+                    class="workflow-card-indicators"
                     :workflow="workflow"
                     :published-view="publishedView"
                     :filterable="props.filterable"
-                    @update-filter="(k, v) => emit('updateFilter', k, v)" />
+                    @updateFilter="(k, v) => emit('updateFilter', k, v)" />
 
                 <div class="workflow-count-actions">
                     <WorkflowInvocationsCount
@@ -125,8 +91,9 @@ async function onTagClick(tag: string) {
                         :workflow="workflow" />
 
                     <WorkflowActions
-                        :workflow="workflow"
-                        :published="publishedView"
+                        :workflow="props.workflow"
+                        :published="props.publishedView"
+                        :editor="props.editorView"
                         @refreshList="emit('refreshList', true)" />
                 </div>
 
@@ -169,44 +136,11 @@ async function onTagClick(tag: string) {
                         @tag-click="onTagClick($event)" />
                 </div>
 
-                <div class="workflow-card-actions">
-                    <WorkflowActionsExtend
-                        :workflow="workflow"
-                        :published="publishedView"
-                        @refreshList="emit('refreshList', true)" />
-
-                    <div class="workflow-edit-run-buttons">
-                        <BButton
-                            v-if="!isAnonymous && !shared"
-                            v-b-tooltip.hover.noninteractive
-                            :disabled="workflow.deleted"
-                            size="sm"
-                            class="workflow-edit-button"
-                            :title="editButtonTitle"
-                            variant="outline-primary"
-                            :to="`/workflows/edit?id=${workflow.id}`">
-                            <FontAwesomeIcon :icon="faEdit" fixed-width />
-                            Edit
-                        </BButton>
-
-                        <AsyncButton
-                            v-else
-                            v-b-tooltip.hover.noninteractive
-                            size="sm"
-                            :disabled="isAnonymous"
-                            :title="importedButtonTitle"
-                            :icon="faUpload"
-                            variant="outline-primary"
-                            :action="onImport">
-                            Import
-                        </AsyncButton>
-
-                        <WorkflowRunButton
-                            :id="workflow.id"
-                            :disabled="isAnonymous || workflow.deleted"
-                            :title="runButtonTitle" />
-                    </div>
-                </div>
+                <WorkflowActionsExtend
+                    :workflow="workflow"
+                    :published="publishedView"
+                    :editor="editorView"
+                    @refreshList="emit('refreshList', true)" />
             </div>
         </div>
     </div>
@@ -248,16 +182,23 @@ async function onTagClick(tag: string) {
         .workflow-card-header {
             display: grid;
             position: relative;
+            grid-template-areas:
+                "i b"
+                "n n";
+
+            .workflow-card-indicators {
+                grid-area: i;
+            }
 
             .workflow-count-actions {
+                grid-area: b;
                 display: flex;
                 align-items: center;
                 flex-direction: row;
-                position: absolute;
-                right: 0.5rem;
             }
 
             .workflow-name {
+                grid-area: n;
                 font-size: 1rem;
                 font-weight: bold;
                 word-break: break-all;
@@ -272,14 +213,6 @@ async function onTagClick(tag: string) {
             .workflow-card-tags {
                 max-width: 60%;
             }
-
-            .workflow-card-actions {
-                display: flex;
-                gap: 0.25rem;
-                margin-top: 0.25rem;
-                align-items: center;
-                justify-content: end;
-            }
         }
 
         @container workflow-card (max-width: #{$breakpoint-sm}) {
@@ -291,18 +224,6 @@ async function onTagClick(tag: string) {
                 }
             }
         }
-
-        @container workflow-card (max-width: #{$breakpoint-sm}) {
-            .workflow-card-actions {
-                justify-content: space-between;
-            }
-        }
-
-        @container workflow-card (min-width: #{$breakpoint-sm}, max-width: #{$breakpoint-md}) {
-            .workflow-card-actions {
-                justify-content: end;
-            }
-        }
     }
 
     @container workflow-card (max-width: #{$breakpoint-md}) {
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index 74e913d444f8..86c9ff8eeb73 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -14,6 +14,7 @@ interface Props {
     hideRuns?: boolean;
     filterable?: boolean;
     publishedView?: boolean;
+    editorView?: boolean;
 }
 
 const props = defineProps<Props>();
@@ -65,6 +66,7 @@ function onPreview(id: string) {
             :hide-runs="props.hideRuns"
             :filterable="props.filterable"
             :published-view="props.publishedView"
+            :editor-view="props.editorView"
             class="workflow-card"
             @tagClick="(...args) => emit('tagClick', ...args)"
             @refreshList="(...args) => emit('refreshList', ...args)"
diff --git a/client/src/components/Workflow/List/WorkflowIndicators.vue b/client/src/components/Workflow/List/WorkflowIndicators.vue
index 2f863a379c6d..300e989d3c31 100644
--- a/client/src/components/Workflow/List/WorkflowIndicators.vue
+++ b/client/src/components/Workflow/List/WorkflowIndicators.vue
@@ -6,7 +6,7 @@ import { BBadge, BButton } from "bootstrap-vue";
 import { computed } from "vue";
 import { useRouter } from "vue-router/composables";
 
-import { Toast } from "@/composables/toast";
+import { useToast } from "@/composables/toast";
 import { useUserStore } from "@/stores/userStore";
 import { copy } from "@/utils/clipboard";
 
@@ -14,8 +14,11 @@ import UtcDate from "@/components/UtcDate.vue";
 
 library.add(faFileImport, faGlobe, faShieldAlt, faUsers, faUser);
 
+// TODO: replace me with a proper definition
+type Workflow = any;
+
 interface Props {
-    workflow: any;
+    workflow: Workflow;
     publishedView: boolean;
     noEditTime?: boolean;
     filterable?: boolean;
@@ -24,7 +27,7 @@ interface Props {
 const props = defineProps<Props>();
 
 const emit = defineEmits<{
-    (e: "update-filter", key: string, value: any): void;
+    (e: "updateFilter", key: string, value: string | boolean): void;
 }>();
 
 const router = useRouter();
@@ -67,36 +70,46 @@ const sourceTitle = computed(() => {
     }
 });
 
+const { success } = useToast();
+
 function onCopyLink() {
     if (sourceType.value == "url") {
         copy(props.workflow.source_metadata.url);
-        Toast.success("URL copied");
+        success("URL copied");
     } else if (sourceType.value.includes("trs")) {
         copy(props.workflow.source_metadata.trs_tool_id);
-        Toast.success("TRS ID copied");
+        success("TRS ID copied");
     }
 }
 
 function onViewMySharedByUser() {
     router.push(`/workflows/list_shared_with_me?owner=${props.workflow.owner}`);
-    emit("update-filter", "user", `'${props.workflow.owner}'`);
+    emit("updateFilter", "user", `'${props.workflow.owner}'`);
 }
 
 function onViewUserPublished() {
     router.push(`/workflows/list_published?owner=${props.workflow.owner}`);
-    emit("update-filter", "user", `'${props.workflow.owner}'`);
+    emit("updateFilter", "user", `'${props.workflow.owner}'`);
+}
+
+function getStepText(steps: number) {
+    if (steps === 1) {
+        return "1 step";
+    } else {
+        return `${steps} steps`;
+    }
 }
 </script>
 
 <template>
-    <div>
+    <div class="workflow-indicators">
         <BButton
             v-if="workflow.published && !publishedView"
             v-b-tooltip.noninteractive.hover
             size="sm"
             class="workflow-published-icon inline-icon-button"
             :title="publishedTitle"
-            @click="emit('update-filter', 'published', true)">
+            @click="emit('updateFilter', 'published', true)">
             <FontAwesomeIcon :icon="faGlobe" fixed-width />
         </BButton>
         <FontAwesomeIcon
@@ -132,6 +145,10 @@ function onViewUserPublished() {
             </small>
         </span>
 
+        <BBadge v-if="workflow.number_of_steps" pill class="mr-1 step-count">
+            {{ getStepText(workflow.number_of_steps) }}
+        </BBadge>
+
         <BBadge
             v-if="shared && !publishedView"
             v-b-tooltip.noninteractive.hover
@@ -158,14 +175,15 @@ function onViewUserPublished() {
 <style scoped lang="scss">
 @import "theme/blue.scss";
 
-.workflow-badge {
-    background: $brand-secondary;
-    color: $brand-primary;
-    border: 1px solid $brand-primary;
+.workflow-indicators {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+}
 
-    &:hover {
-        color: $brand-secondary;
-        background: $brand-primary;
-    }
+.step-count {
+    display: grid;
+    place-items: center;
+    height: 1rem;
 }
 </style>
diff --git a/client/src/components/Workflow/List/useWorkflowActions.ts b/client/src/components/Workflow/List/useWorkflowActions.ts
new file mode 100644
index 000000000000..8b0855fb4bc0
--- /dev/null
+++ b/client/src/components/Workflow/List/useWorkflowActions.ts
@@ -0,0 +1,110 @@
+import { computed, type Ref, ref } from "vue";
+
+import {
+    copyWorkflow as copyWorkflowService,
+    deleteWorkflow as deleteWorkflowService,
+    updateWorkflow as updateWorkflowService,
+} from "@/components/Workflow/workflows.services";
+import { useConfig } from "@/composables/config";
+import { useConfirmDialog } from "@/composables/confirmDialog";
+import { useToast } from "@/composables/toast";
+import { copy } from "@/utils/clipboard";
+import { withPrefix } from "@/utils/redirect";
+import { getFullAppUrl } from "@/utils/utils";
+
+// TODO: replace me with a more accurate type
+type Workflow = any;
+
+export function useWorkflowActions(workflow: Ref<Workflow>, refreshCallback: () => void) {
+    const toast = useToast();
+    const { config } = useConfig() as { config: Record<string, any> };
+
+    const bookmarkLoading = ref(false);
+
+    const toggleBookmark = async (checked: boolean) => {
+        try {
+            bookmarkLoading.value = true;
+
+            await updateWorkflowService(workflow.value.id, {
+                show_in_tool_panel: checked,
+            });
+
+            toast.info(`Workflow ${checked ? "added to" : "removed from"} bookmarks`);
+
+            if (checked) {
+                config.stored_workflow_menu_entries.push({
+                    id: workflow.value.id,
+                    name: workflow.value.name,
+                });
+            } else {
+                const indexToRemove = config.stored_workflow_menu_entries.findIndex(
+                    (w: Workflow) => w.id === workflow.value.id
+                );
+                config.stored_workflow_menu_entries.splice(indexToRemove, 1);
+            }
+        } catch (error) {
+            toast.error("Failed to update workflow bookmark status");
+        } finally {
+            refreshCallback();
+            bookmarkLoading.value = false;
+        }
+    };
+
+    const { confirm } = useConfirmDialog();
+
+    const deleteWorkflow = async () => {
+        const confirmed = await confirm("Are you sure you want to delete this workflow?", {
+            title: "Delete workflow",
+            okTitle: "Delete",
+            okVariant: "danger",
+        });
+
+        if (confirmed) {
+            await deleteWorkflowService(workflow.value.id);
+            refreshCallback();
+            toast.info("Workflow deleted");
+        }
+    };
+
+    const relativeLink = computed(() => {
+        return `/published/workflow?id=${workflow.value.id}`;
+    });
+
+    const fullLink = computed(() => {
+        return getFullAppUrl(relativeLink.value.substring(1));
+    });
+
+    function copyPublicLink() {
+        copy(fullLink.value);
+        toast.success("Link to workflow copied");
+    }
+
+    async function copyWorkflow() {
+        const confirmed = await confirm("Are you sure you want to make a copy of this workflow?", "Copy workflow");
+
+        if (confirmed) {
+            await copyWorkflowService(workflow.value.id, workflow.value.owner);
+            refreshCallback();
+            toast.success("Workflow copied");
+        }
+    }
+
+    const downloadUrl = computed(() => {
+        return withPrefix(`/api/workflows/${workflow.value.id}/download?format=json-download`);
+    });
+
+    async function importWorkflow() {
+        await copyWorkflowService(workflow.value.id, workflow.value.owner);
+        toast.success("Workflow imported successfully");
+    }
+
+    return {
+        bookmarkLoading,
+        toggleBookmark,
+        deleteWorkflow,
+        copyPublicLink,
+        copyWorkflow,
+        importWorkflow,
+        downloadUrl,
+    };
+}
diff --git a/client/src/components/Workflow/WorkflowInvocationsCount.vue b/client/src/components/Workflow/WorkflowInvocationsCount.vue
index 275173da3eb9..e9101f5def7a 100644
--- a/client/src/components/Workflow/WorkflowInvocationsCount.vue
+++ b/client/src/components/Workflow/WorkflowInvocationsCount.vue
@@ -41,7 +41,7 @@ onMounted(initCounts);
 
 <template>
     <div class="workflow-invocations-count d-flex align-items-center flex-gapx-1">
-        <BBadge v-if="count != undefined && count === 0" pill class="list-view">
+        <BBadge v-if="count != undefined && count === 0" pill>
             <span>never run</span>
         </BBadge>
         <BBadge
@@ -49,7 +49,7 @@ onMounted(initCounts);
             v-b-tooltip.hover.noninteractive
             pill
             :title="localize('View workflow invocations')"
-            class="outline-badge cursor-pointer list-view"
+            class="outline-badge cursor-pointer"
             :to="`/workflows/${props.workflow.id}/invocations`">
             <FontAwesomeIcon :icon="faList" fixed-width />
 
diff --git a/client/src/components/Workflow/workflows.services.ts b/client/src/components/Workflow/workflows.services.ts
index 1a39e98d57f0..7c611620c807 100644
--- a/client/src/components/Workflow/workflows.services.ts
+++ b/client/src/components/Workflow/workflows.services.ts
@@ -1,10 +1,14 @@
 import axios from "axios";
 
+import { GalaxyApi } from "@/api";
 import { useUserStore } from "@/stores/userStore";
 import { withPrefix } from "@/utils/redirect";
+import { rethrowSimple } from "@/utils/simple-error";
 
 export type Workflow = Record<string, never>;
 
+type SortBy = "create_time" | "update_time" | "name";
+
 interface LoadWorkflowsOptions {
     sortBy: SortBy;
     sortDesc: boolean;
@@ -15,7 +19,6 @@ interface LoadWorkflowsOptions {
     skipStepCounts: boolean;
 }
 
-const getWorkflows = fetcher.path("/api/workflows").method("get").create();
 export async function loadWorkflows({
     sortBy = "update_time",
     sortDesc = true,
@@ -24,17 +27,28 @@ export async function loadWorkflows({
     filterText = "",
     showPublished = false,
     skipStepCounts = true,
-}: LoadWorkflowsOptions): Promise<{ data: Workflow[]; headers: Headers }> {
-    const { data, headers } = await getWorkflows({
-        sort_by: sortBy,
-        sort_desc: sortDesc,
-        limit,
-        offset,
-        search: filterText,
-        show_published: showPublished,
-        skip_step_counts: skipStepCounts,
+}: LoadWorkflowsOptions): Promise<{ data: Workflow[]; totalMatches: number }> {
+    const { response, data, error } = await GalaxyApi().GET("/api/workflows", {
+        params: {
+            query: {
+                sort_by: sortBy,
+                sort_desc: sortDesc,
+                limit,
+                offset,
+                search: filterText,
+                show_published: showPublished,
+                skip_step_counts: skipStepCounts,
+            },
+        },
     });
-    return { data, headers };
+
+    if (error) {
+        rethrowSimple(error);
+    }
+
+    const totalMatches = parseInt(response.headers.get("Total_matches") || "0", 10) || 0;
+
+    return { data, totalMatches };
 }
 
 export async function updateWorkflow(id: string, changes: object): Promise<Workflow> {

From c460dd75b32897e830a4c31df2c6c1b828333643 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 6 Sep 2024 14:27:44 +0200
Subject: [PATCH 015/131] fix z-indexing for dropdown add missing actions to
 dropdown

---
 .../Workflow/List/WorkflowActions.vue         | 55 ++++++++-----------
 .../components/Workflow/List/WorkflowCard.vue | 13 ++++-
 2 files changed, 33 insertions(+), 35 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue
index a80fbcab2f02..bbedf649ab0a 100644
--- a/client/src/components/Workflow/List/WorkflowActions.vue
+++ b/client/src/components/Workflow/List/WorkflowActions.vue
@@ -3,10 +3,12 @@ import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
 import {
     faCaretDown,
     faCopy,
+    faDownload,
     faExternalLinkAlt,
     faFileExport,
     faLink,
     faPlay,
+    faShareAlt,
     faSpinner,
     faStar,
     faTrash,
@@ -33,12 +35,14 @@ const props = withDefaults(defineProps<Props>(), {
 
 const emit = defineEmits<{
     (e: "refreshList", overlayLoading?: boolean): void;
+    (e: "dropdown", open: boolean): void;
 }>();
 
-const { bookmarkLoading, deleteWorkflow, toggleBookmark, copyPublicLink, copyWorkflow } = useWorkflowActions(
-    computed(() => props.workflow),
-    () => emit("refreshList", true)
-);
+const { bookmarkLoading, deleteWorkflow, toggleBookmark, copyPublicLink, copyWorkflow, downloadUrl } =
+    useWorkflowActions(
+        computed(() => props.workflow),
+        () => emit("refreshList", true)
+    );
 
 const userStore = useUserStore();
 const { isAnonymous } = storeToRefs(userStore);
@@ -100,7 +104,9 @@ const runPath = computed(
                 class="workflow-actions-dropdown"
                 title="Workflow actions"
                 toggle-class="inline-icon-button"
-                variant="link">
+                variant="link"
+                @show="() => emit('dropdown', true)"
+                @hide="() => emit('dropdown', false)">
                 <template v-slot:button-content>
                     <FontAwesomeIcon :icon="faCaretDown" fixed-width />
                 </template>
@@ -115,44 +121,33 @@ const runPath = computed(
                         v-if="props.workflow.published"
                         size="sm"
                         title="Copy link to workflow"
-                        variant="link"
                         @click="copyPublicLink">
                         <FontAwesomeIcon :icon="faLink" fixed-width />
                         Link to Workflow
                     </BDropdownItem>
 
-                    <BDropdownItem
-                        v-if="!isAnonymous && !shared"
-                        size="sm"
-                        title="Copy workflow"
-                        variant="link"
-                        @click="copyWorkflow">
+                    <BDropdownItem v-if="!isAnonymous && !shared" size="sm" title="Copy workflow" @click="copyWorkflow">
                         <FontAwesomeIcon :icon="faCopy" fixed-width />
                         Copy
                     </BDropdownItem>
 
-                    <!--BButton
-                        id="workflow-download-button"
-                        v-b-tooltip.hover.noninteractive
+                    <BDropdownItem
                         size="sm"
                         title="Download workflow in .ga format"
-                        variant="outline-primary"
+                        target="_blank"
                         :href="downloadUrl">
                         <FontAwesomeIcon :icon="faDownload" fixed-width />
-                        <span class="compact-view">Download</span>
-                    </BButton>
+                        Download
+                    </BDropdownItem>
 
-                    <BButton
+                    <BDropdownItem
                         v-if="!isAnonymous && !shared"
-                        id="workflow-share-button"
-                        v-b-tooltip.hover.noninteractive
                         size="sm"
                         title="Share"
-                        variant="outline-primary"
                         :to="`/workflows/sharing?id=${workflow.id}`">
                         <FontAwesomeIcon :icon="faShareAlt" fixed-width />
-                        <span class="compact-view">Share</span>
-                    </BButton-->
+                        Share
+                    </BDropdownItem>
                 </template>
 
                 <BDropdownItem
@@ -160,10 +155,9 @@ const runPath = computed(
                     class="workflow-delete-button"
                     title="Delete workflow"
                     size="sm"
-                    variant="link"
                     @click="deleteWorkflow">
                     <FontAwesomeIcon :icon="faTrash" fixed-width />
-                    <span>Delete</span>
+                    Delete
                 </BDropdownItem>
 
                 <BDropdownItem
@@ -171,11 +165,10 @@ const runPath = computed(
                     class="source-trs-button"
                     :title="`View on ${props.workflow.source_metadata?.trs_server}`"
                     size="sm"
-                    variant="link"
                     :href="`https://dockstore.org/workflows${props.workflow?.source_metadata?.trs_tool_id?.slice(9)}`"
                     target="_blank">
                     <FontAwesomeIcon :icon="faExternalLinkAlt" fixed-width />
-                    <span>View on Dockstore</span>
+                    View on Dockstore
                 </BDropdownItem>
 
                 <BDropdownItem
@@ -183,11 +176,10 @@ const runPath = computed(
                     class="workflow-view-external-link-button"
                     title="View external link"
                     size="sm"
-                    variant="link"
                     :href="props.workflow.source_metadata?.url"
                     target="_blank">
                     <FontAwesomeIcon :icon="faExternalLinkAlt" fixed-width />
-                    <span>View external link</span>
+                    View external link
                 </BDropdownItem>
 
                 <BDropdownItem
@@ -195,10 +187,9 @@ const runPath = computed(
                     class="workflow-export-button"
                     title="Export"
                     size="sm"
-                    variant="link"
                     :to="`/workflows/export?id=${props.workflow.id}`">
                     <FontAwesomeIcon :icon="faFileExport" fixed-width />
-                    <span>Export</span>
+                    Export
                 </BDropdownItem>
             </BDropdown>
         </BButtonGroup>
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 953570a7019c..5fbf335957b2 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -3,7 +3,7 @@ import { faPen } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { BButton, BLink } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
-import { computed } from "vue";
+import { computed, ref } from "vue";
 
 import { updateWorkflow } from "@/components/Workflow/workflows.services";
 import { useUserStore } from "@/stores/userStore";
@@ -67,10 +67,12 @@ async function onTagsUpdate(tags: string[]) {
 async function onTagClick(tag: string) {
     emit("tagClick", tag);
 }
+
+const dropdownOpen = ref(false);
 </script>
 
 <template>
-    <div class="workflow-card" :data-workflow-id="workflow.id">
+    <div class="workflow-card" :class="{ 'dropdown-open': dropdownOpen }" :data-workflow-id="workflow.id">
         <div
             class="workflow-card-container"
             :class="{
@@ -94,7 +96,8 @@ async function onTagClick(tag: string) {
                         :workflow="props.workflow"
                         :published="props.publishedView"
                         :editor="props.editorView"
-                        @refreshList="emit('refreshList', true)" />
+                        @refreshList="emit('refreshList', true)"
+                        @dropdown="(open) => (dropdownOpen = open)" />
                 </div>
 
                 <span class="workflow-name font-weight-bold">
@@ -154,6 +157,10 @@ async function onTagClick(tag: string) {
     container: workflow-card / inline-size;
     padding: 0 0.25rem 0.5rem 0.25rem;
 
+    &.dropdown-open {
+        z-index: 10;
+    }
+
     .workflow-rename {
         opacity: 0;
     }

From 859ff833008567e5eeacf6e27dd2450660f2b4aa Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 6 Sep 2024 14:40:07 +0200
Subject: [PATCH 016/131] connect insert and insert steps events

---
 client/src/components/Panels/WorkflowPanel.vue    | 13 ++++++++++++-
 client/src/components/Workflow/Editor/Index.vue   |  5 ++++-
 .../Workflow/List/WorkflowActionsExtend.vue       |  4 ++--
 .../src/components/Workflow/List/WorkflowCard.vue |  6 +++++-
 .../components/Workflow/List/WorkflowCardList.vue | 15 ++++++++++++++-
 5 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index ec20955e0745..b301f01a5bc6 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -11,6 +11,11 @@ import DelayedInput from "@/components/Common/DelayedInput.vue";
 import ScrollToTopButton from "@/components/ToolsList/ScrollToTopButton.vue";
 import WorkflowCardList from "@/components/Workflow/List/WorkflowCardList.vue";
 
+const emit = defineEmits<{
+    (e: "insertWorkflow", id: string, name: string): void;
+    (e: "insertWorkflowSteps", id: string, stepCount: number): void;
+}>();
+
 const scrollable = ref<HTMLDivElement | null>(null);
 const { arrived, scrollTop } = useAnimationFrameScroll(scrollable);
 
@@ -115,7 +120,13 @@ function scrollToTop() {
             :loading="loading"></DelayedInput>
 
         <div ref="scrollable" class="workflow-scroll-list mt-2">
-            <WorkflowCardList :hide-runs="true" :workflows="workflows" :filterable="false" editor-view />
+            <WorkflowCardList
+                :hide-runs="true"
+                :workflows="workflows"
+                :filterable="false"
+                editor-view
+                @insertWorkflow="(...args) => emit('insertWorkflow', ...args)"
+                @insertWorkflowSteps="(...args) => emit('insertWorkflowSteps', ...args)" />
 
             <div v-if="allLoaded || filterText !== ''" class="list-end">
                 <span v-if="workflows.length == 1"> - 1 workflow loaded - </span>
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 0dddc75d8005..9ad9f6528472 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -58,7 +58,10 @@
                     @onRefactor="onAttemptRefactor"
                     @onScrollTo="onScrollTo" />
                 <UndoRedoStack v-else-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
-                <WorkflowPanel v-else-if="isActiveSideBar('workflow-editor-workflows')"></WorkflowPanel>
+                <WorkflowPanel
+                    v-else-if="isActiveSideBar('workflow-editor-workflows')"
+                    @insertWorkflow="onInsertWorkflow"
+                    @insertWorkflowSteps="onInsertWorkflowSteps" />
             </template>
         </ActivityBar>
         <div id="center" class="workflow-center">
diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
index f896fe89ceff..9992e4c33bcd 100644
--- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue
+++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
 const emit = defineEmits<{
     (e: "refreshList", overlayLoading?: boolean): void;
     (e: "insert"): void;
-    (e: "copySteps"): void;
+    (e: "insertSteps"): void;
 }>();
 
 const userStore = useUserStore();
@@ -198,7 +198,7 @@ const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflo
                     size="sm"
                     title="Copy steps into workflow"
                     variant="outline-primary"
-                    @click="emit('copySteps')">
+                    @click="emit('insertSteps')">
                     <FontAwesomeIcon :icon="faCopy" fixed-width />
                 </BButton>
 
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 5fbf335957b2..6bfc210df4f7 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -38,6 +38,8 @@ const emit = defineEmits<{
     (e: "updateFilter", key: string, value: any): void;
     (e: "rename", id: string, name: string): void;
     (e: "preview", id: string): void;
+    (e: "insert"): void;
+    (e: "insertSteps"): void;
 }>();
 
 const userStore = useUserStore();
@@ -143,7 +145,9 @@ const dropdownOpen = ref(false);
                     :workflow="workflow"
                     :published="publishedView"
                     :editor="editorView"
-                    @refreshList="emit('refreshList', true)" />
+                    @refreshList="emit('refreshList', true)"
+                    @insert="(...args) => emit('insert', ...args)"
+                    @insertSteps="(...args) => emit('insertSteps', ...args)" />
             </div>
         </div>
     </div>
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index 86c9ff8eeb73..7b4aaf0cc61f 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -23,6 +23,8 @@ const emit = defineEmits<{
     (e: "tagClick", tag: string): void;
     (e: "refreshList", overlayLoading?: boolean, silent?: boolean): void;
     (e: "updateFilter", key: string, value: any): void;
+    (e: "insertWorkflow", id: string, name: string): void;
+    (e: "insertWorkflowSteps", id: string, stepCount: number): void;
 }>();
 
 const modalOptions = reactive({
@@ -54,6 +56,15 @@ function onPreview(id: string) {
     modalOptions.preview.id = id;
     showPreview.value = true;
 }
+
+// TODO: clean-up types, as soon as better Workflow type is available
+function onInsert(workflow: Workflow) {
+    emit("insertWorkflow", workflow.id as any, workflow.name as any);
+}
+
+function onInsertSteps(workflow: Workflow) {
+    emit("insertWorkflowSteps", workflow.id as any, workflow.number_of_steps as any);
+}
 </script>
 
 <template>
@@ -72,7 +83,9 @@ function onPreview(id: string) {
             @refreshList="(...args) => emit('refreshList', ...args)"
             @updateFilter="(...args) => emit('updateFilter', ...args)"
             @rename="onRename"
-            @preview="onPreview">
+            @preview="onPreview"
+            @insert="onInsert(workflow)"
+            @insertSteps="onInsertSteps(workflow)">
         </WorkflowCard>
 
         <WorkflowRename

From c642f033bac722d7ca5b6ae35b1917a7d37ddbc1 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 6 Sep 2024 15:14:14 +0200
Subject: [PATCH 017/131] move breakpoints to partial remove unused css fix
 broken css add non-found indicator

---
 .../src/components/Panels/WorkflowPanel.vue   |  1 +
 .../Workflow/List/WorkflowActionsExtend.vue   |  2 +-
 .../components/Workflow/List/WorkflowCard.vue | 39 +++++++++++--------
 .../Workflow/List/WorkflowCardList.vue        |  2 +-
 .../Workflow/List/WorkflowIndicators.vue      |  1 -
 .../components/Workflow/List/WorkflowList.vue |  3 --
 .../Workflow/List/_breakpoints.scss}          |  1 +
 7 files changed, 26 insertions(+), 23 deletions(-)
 rename client/src/{style/scss/breakpoints.scss => components/Workflow/List/_breakpoints.scss} (83%)

diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index b301f01a5bc6..4028c896682d 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -131,6 +131,7 @@ function scrollToTop() {
             <div v-if="allLoaded || filterText !== ''" class="list-end">
                 <span v-if="workflows.length == 1"> - 1 workflow loaded - </span>
                 <span v-else-if="workflows.length > 1"> - All {{ workflows.length }} workflows loaded - </span>
+                <span v-else> - No workflows found - </span>
             </div>
             <div v-else-if="loading" class="list-end">- loading -</div>
         </div>
diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
index 9992e4c33bcd..abe6ed959ea7 100644
--- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue
+++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
@@ -217,7 +217,7 @@ const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflo
 </template>
 
 <style scoped lang="scss">
-@import "breakpoints.scss";
+@import "_breakpoints.scss";
 
 .workflow-card-actions {
     display: flex;
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 6bfc210df4f7..90abc904fbfb 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -125,7 +125,7 @@ const dropdownOpen = ref(false);
 
                 <TextSummary
                     v-if="description"
-                    class="my-1"
+                    class="workflow-summary my-1"
                     :description="description"
                     :max-length="gridView ? 100 : 250" />
             </div>
@@ -155,7 +155,7 @@ const dropdownOpen = ref(false);
 
 <style scoped lang="scss">
 @import "theme/blue.scss";
-@import "breakpoints.scss";
+@import "_breakpoints.scss";
 
 .workflow-card {
     container: workflow-card / inline-size;
@@ -193,9 +193,18 @@ const dropdownOpen = ref(false);
         .workflow-card-header {
             display: grid;
             position: relative;
+            align-items: start;
             grid-template-areas:
                 "i b"
-                "n n";
+                "n n"
+                "s s";
+
+            @container workflow-card (max-width: #{$breakpoint-xs}) {
+                grid-template-areas:
+                    "i b"
+                    "n b"
+                    "s s";
+            }
 
             .workflow-card-indicators {
                 grid-area: i;
@@ -206,6 +215,12 @@ const dropdownOpen = ref(false);
                 display: flex;
                 align-items: center;
                 flex-direction: row;
+                justify-content: end;
+
+                @container workflow-card (max-width: #{$breakpoint-xs}) {
+                    align-items: end;
+                    flex-direction: column-reverse;
+                }
             }
 
             .workflow-name {
@@ -214,6 +229,10 @@ const dropdownOpen = ref(false);
                 font-weight: bold;
                 word-break: break-all;
             }
+
+            .workflow-summary {
+                grid-area: s;
+            }
         }
 
         .workflow-card-footer {
@@ -236,19 +255,5 @@ const dropdownOpen = ref(false);
             }
         }
     }
-
-    @container workflow-card (max-width: #{$breakpoint-md}) {
-        .workflow-count-actions {
-            align-items: baseline;
-            justify-content: end;
-        }
-    }
-
-    @container workflow-card (min-width: #{$breakpoint-md}) {
-        .workflow-count-actions {
-            align-items: end;
-            flex-direction: column-reverse;
-        }
-    }
 }
 </style>
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index 7b4aaf0cc61f..91a3b9f91151 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -117,7 +117,7 @@ function onInsertSteps(workflow: Workflow) {
 </style>
 
 <style scoped lang="scss">
-@import "breakpoints.scss";
+@import "_breakpoints.scss";
 
 .workflow-card-list {
     container: card-list / inline-size;
diff --git a/client/src/components/Workflow/List/WorkflowIndicators.vue b/client/src/components/Workflow/List/WorkflowIndicators.vue
index 300e989d3c31..d63617f852b5 100644
--- a/client/src/components/Workflow/List/WorkflowIndicators.vue
+++ b/client/src/components/Workflow/List/WorkflowIndicators.vue
@@ -177,7 +177,6 @@ function getStepText(steps: number) {
 
 .workflow-indicators {
     display: flex;
-    flex-wrap: wrap;
     align-items: center;
 }
 
diff --git a/client/src/components/Workflow/List/WorkflowList.vue b/client/src/components/Workflow/List/WorkflowList.vue
index 06def93b18da..327c514bd495 100644
--- a/client/src/components/Workflow/List/WorkflowList.vue
+++ b/client/src/components/Workflow/List/WorkflowList.vue
@@ -329,9 +329,6 @@ onMounted(() => {
 </template>
 
 <style lang="scss">
-@import "scss/mixins.scss";
-@import "breakpoints.scss";
-
 .workflows-list {
     overflow: auto;
     display: flex;
diff --git a/client/src/style/scss/breakpoints.scss b/client/src/components/Workflow/List/_breakpoints.scss
similarity index 83%
rename from client/src/style/scss/breakpoints.scss
rename to client/src/components/Workflow/List/_breakpoints.scss
index 641b39ff9d2b..d758c7980ec2 100644
--- a/client/src/style/scss/breakpoints.scss
+++ b/client/src/components/Workflow/List/_breakpoints.scss
@@ -1,3 +1,4 @@
+$breakpoint-xs: 300px;
 $breakpoint-sm: 576px;
 $breakpoint-md: 768px;
 $breakpoint-lg: 992px;

From 0f4eb0f6bcbf3570a6719041db0fcb7a5444e927 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 9 Sep 2024 11:26:47 +0200
Subject: [PATCH 018/131] only occupy vertical space if invocations count is
 present

---
 .../src/components/Workflow/List/WorkflowCard.vue  | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 90abc904fbfb..4a8370e2bfe6 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -91,7 +91,7 @@ const dropdownOpen = ref(false);
                 <div class="workflow-count-actions">
                     <WorkflowInvocationsCount
                         v-if="!props.hideRuns && !isAnonymous && !shared"
-                        class="mx-1"
+                        class="invocations-count mx-1"
                         :workflow="workflow" />
 
                     <WorkflowActions
@@ -199,11 +199,13 @@ const dropdownOpen = ref(false);
                 "n n"
                 "s s";
 
-            @container workflow-card (max-width: #{$breakpoint-xs}) {
-                grid-template-areas:
-                    "i b"
-                    "n b"
-                    "s s";
+            &:has(.invocations-count) {
+                @container workflow-card (max-width: #{$breakpoint-xs}) {
+                    grid-template-areas:
+                        "i b"
+                        "n b"
+                        "s s";
+                }
             }
 
             .workflow-card-indicators {

From bd36e02503e11a5a5ae33939cb8eb563872870e8 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 9 Sep 2024 11:33:47 +0200
Subject: [PATCH 019/131] rename Attributes to WorkflowAttributes

---
 client/src/components/Workflow/Editor/Index.vue             | 2 +-
 .../Editor/{Attributes.vue => WorkflowAttributes.vue}       | 6 +-----
 2 files changed, 2 insertions(+), 6 deletions(-)
 rename client/src/components/Workflow/Editor/{Attributes.vue => WorkflowAttributes.vue} (98%)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 9ad9f6528472..d978a8d2aeb3 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -223,13 +223,13 @@ import { getModule, getVersions, loadWorkflow, saveWorkflow } from "./modules/se
 import { getStateUpgradeMessages } from "./modules/utilities";
 import reportDefault from "./reportDefault";
 
-import WorkflowAttributes from "./Attributes.vue";
 import WorkflowLint from "./Lint.vue";
 import MessagesModal from "./MessagesModal.vue";
 import WorkflowOptions from "./Options.vue";
 import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
 import SaveChangesModal from "./SaveChangesModal.vue";
 import StateUpgradeModal from "./StateUpgradeModal.vue";
+import WorkflowAttributes from "./WorkflowAttributes.vue";
 import WorkflowGraph from "./WorkflowGraph.vue";
 import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
diff --git a/client/src/components/Workflow/Editor/Attributes.vue b/client/src/components/Workflow/Editor/WorkflowAttributes.vue
similarity index 98%
rename from client/src/components/Workflow/Editor/Attributes.vue
rename to client/src/components/Workflow/Editor/WorkflowAttributes.vue
index c0d7311e06ee..e0fc558b1c6b 100644
--- a/client/src/components/Workflow/Editor/Attributes.vue
+++ b/client/src/components/Workflow/Editor/WorkflowAttributes.vue
@@ -56,20 +56,16 @@
 </template>
 
 <script>
-import BootstrapVue from "bootstrap-vue";
 import LicenseSelector from "components/License/LicenseSelector";
 import CreatorEditor from "components/SchemaOrg/CreatorEditor";
 import StatelessTags from "components/TagsMultiselect/StatelessTags";
 import { Services } from "components/Workflow/services";
 import { format, parseISO } from "date-fns";
-import Vue from "vue";
 
 import { UntypedParameters } from "./modules/parameters";
 
-Vue.use(BootstrapVue);
-
 export default {
-    name: "Attributes",
+    name: "WorkflowAttributes",
     components: {
         StatelessTags,
         LicenseSelector,

From 2f9f9c2a9f46374b8c8beaaa6a418047e52ff404 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 9 Sep 2024 11:45:18 +0200
Subject: [PATCH 020/131] make attributes an activity

---
 .../src/components/Workflow/Editor/Index.vue  | 34 +++++++++----------
 .../Workflow/Editor/WorkflowAttributes.vue    | 24 +++++++------
 2 files changed, 31 insertions(+), 27 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index d978a8d2aeb3..b5bade5a9b0b 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -62,6 +62,23 @@
                     v-else-if="isActiveSideBar('workflow-editor-workflows')"
                     @insertWorkflow="onInsertWorkflow"
                     @insertWorkflowSteps="onInsertWorkflowSteps" />
+                <WorkflowAttributes
+                    v-else-if="isActiveSideBar('workflow-editor-attributes')"
+                    :id="id"
+                    :tags="tags"
+                    :parameters="parameters"
+                    :annotation="annotation"
+                    :name="name"
+                    :version="version"
+                    :versions="versions"
+                    :license="license"
+                    :creator="creator"
+                    @version="onVersion"
+                    @tags="setTags"
+                    @license="onLicense"
+                    @creator="onCreator"
+                    @update:nameCurrent="setName"
+                    @update:annotationCurrent="setAnnotation" />
             </template>
         </ActivityBar>
         <div id="center" class="workflow-center">
@@ -148,23 +165,6 @@
                             @onAttemptRefactor="onAttemptRefactor"
                             @onSetData="onSetData"
                             @onUpdateStep="updateStep" />
-                        <WorkflowAttributes
-                            v-else-if="showInPanel === 'attributes'"
-                            :id="id"
-                            :tags="tags"
-                            :parameters="parameters"
-                            :annotation="annotation"
-                            :name="name"
-                            :version="version"
-                            :versions="versions"
-                            :license="license"
-                            :creator="creator"
-                            @onVersion="onVersion"
-                            @onTags="setTags"
-                            @onLicense="onLicense"
-                            @onCreator="onCreator"
-                            @update:nameCurrent="setName"
-                            @update:annotationCurrent="setAnnotation" />
                     </div>
                 </div>
             </div>
diff --git a/client/src/components/Workflow/Editor/WorkflowAttributes.vue b/client/src/components/Workflow/Editor/WorkflowAttributes.vue
index e0fc558b1c6b..dfcccc86ded2 100644
--- a/client/src/components/Workflow/Editor/WorkflowAttributes.vue
+++ b/client/src/components/Workflow/Editor/WorkflowAttributes.vue
@@ -1,5 +1,5 @@
 <template>
-    <div id="edit-attributes" class="right-content p-2" itemscope itemtype="http://schema.org/CreativeWork">
+    <ActivityPanel id="edit-attributes" title="Attributes" itemscope itemtype="http://schema.org/CreativeWork">
         <b-alert :variant="messageVariant" :show="!!message">
             {{ message }}
         </b-alert>
@@ -52,24 +52,28 @@
                 Apply tags to make it easy to search for and find items with the same tag.
             </div>
         </div>
-    </div>
+    </ActivityPanel>
 </template>
 
 <script>
-import LicenseSelector from "components/License/LicenseSelector";
-import CreatorEditor from "components/SchemaOrg/CreatorEditor";
-import StatelessTags from "components/TagsMultiselect/StatelessTags";
-import { Services } from "components/Workflow/services";
 import { format, parseISO } from "date-fns";
 
+import { Services } from "@/components/Workflow/services";
+
 import { UntypedParameters } from "./modules/parameters";
 
+import LicenseSelector from "@/components/License/LicenseSelector.vue";
+import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
+import CreatorEditor from "@/components/SchemaOrg/CreatorEditor.vue";
+import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
+
 export default {
     name: "WorkflowAttributes",
     components: {
         StatelessTags,
         LicenseSelector,
         CreatorEditor,
+        ActivityPanel,
     },
     props: {
         id: {
@@ -182,16 +186,16 @@ export default {
     methods: {
         onTags(tags) {
             this.onAttributes({ tags });
-            this.$emit("onTags", tags);
+            this.$emit("tags", tags);
         },
         onVersion() {
-            this.$emit("onVersion", this.versionCurrent);
+            this.$emit("version", this.versionCurrent);
         },
         onLicense(license) {
-            this.$emit("onLicense", license);
+            this.$emit("license", license);
         },
         onCreator(creator) {
-            this.$emit("onCreator", creator);
+            this.$emit("creator", creator);
         },
         onError(error) {
             this.message = error;

From 6c1b64cf5a7362922714282dd29c77b078eb17c5 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 9 Sep 2024 12:15:23 +0200
Subject: [PATCH 021/131] move MarkdownEditor to activity bar

---
 .../components/ActivityBar/ActivityBar.vue    |   5 +
 .../components/Markdown/MarkdownEditor.vue    |  13 +-
 .../components/Markdown/MarkdownToolBox.vue   |  52 ++--
 .../src/components/Workflow/Editor/Index.vue  | 247 +++++++++---------
 .../Workflow/Editor/modules/activities.ts     |  22 +-
 5 files changed, 164 insertions(+), 175 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 851103fe6779..ea059f38813a 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -168,8 +168,13 @@ function onActivityClicked(activity: Activity) {
     }
 }
 
+function setActiveSideBar(key: string) {
+    activityStore.toggledSideBar = key;
+}
+
 defineExpose({
     isActiveSideBar,
+    setActiveSideBar,
 });
 </script>
 
diff --git a/client/src/components/Markdown/MarkdownEditor.vue b/client/src/components/Markdown/MarkdownEditor.vue
index 09ed3bd6e6bb..69a2bec4f265 100644
--- a/client/src/components/Markdown/MarkdownEditor.vue
+++ b/client/src/components/Markdown/MarkdownEditor.vue
@@ -1,8 +1,5 @@
 <template>
     <div id="columns" class="d-flex">
-        <FlexPanel side="left">
-            <MarkdownToolBox :steps="steps" @onInsert="onInsert" />
-        </FlexPanel>
         <div id="center" class="overflow-auto w-100">
             <div class="markdown-editor h-100">
                 <div class="unified-panel-header" unselectable="on">
@@ -42,12 +39,10 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 import { faQuestion } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import BootstrapVue from "bootstrap-vue";
-import FlexPanel from "components/Panels/FlexPanel";
 import _ from "underscore";
 import Vue from "vue";
 
 import MarkdownHelpModal from "./MarkdownHelpModal";
-import MarkdownToolBox from "./MarkdownToolBox";
 
 Vue.use(BootstrapVue);
 
@@ -57,8 +52,6 @@ const FENCE = "```";
 
 export default {
     components: {
-        MarkdownToolBox,
-        FlexPanel,
         FontAwesomeIcon,
         MarkdownHelpModal,
     },
@@ -97,7 +90,7 @@ export default {
         },
     },
     methods: {
-        onInsert(markdown) {
+        insertMarkdown(markdown) {
             markdown = markdown.replace(")(", ", ");
             markdown = `${FENCE}galaxy\n${markdown}\n${FENCE}\n`;
             const textArea = this.$refs["text-area"];
@@ -106,10 +99,10 @@ export default {
             let newContent = this.content.substr(0, cursorPosition);
             newContent += `\r\n${markdown.trim()}\r\n`;
             newContent += this.content.substr(cursorPosition);
-            this.$emit("onUpdate", newContent);
+            this.$emit("update", newContent);
         },
         onUpdate: _.debounce(function (e) {
-            this.$emit("onUpdate", this.content);
+            this.$emit("update", this.content);
         }, 300),
         onHelp() {
             this.$refs.help.showMarkdownHelp();
diff --git a/client/src/components/Markdown/MarkdownToolBox.vue b/client/src/components/Markdown/MarkdownToolBox.vue
index fb2d9f0f91c8..336c5951dce1 100644
--- a/client/src/components/Markdown/MarkdownToolBox.vue
+++ b/client/src/components/Markdown/MarkdownToolBox.vue
@@ -1,32 +1,21 @@
 <template>
-    <div class="unified-panel">
-        <div class="unified-panel-header" unselectable="on">
-            <div class="unified-panel-header-inner">
-                <div class="panel-header-text">Insert Objects</div>
-            </div>
-        </div>
-        <div class="unified-panel-body">
-            <div class="toolMenuContainer">
-                <b-alert v-if="error" variant="danger" class="my-2 mx-3 px-2 py-1" show>
-                    {{ error }}
-                </b-alert>
-                <ToolSection v-if="isWorkflow" :category="historyInEditorSection" :expanded="true" @onClick="onClick" />
-                <ToolSection v-else :category="historySection" :expanded="true" @onClick="onClick" />
-                <ToolSection :category="jobSection" :expanded="true" @onClick="onClick" />
-                <ToolSection
-                    v-if="isWorkflow"
-                    :category="workflowInEditorSection"
-                    :expanded="true"
-                    @onClick="onClick" />
-                <ToolSection v-else :category="workflowSection" :expanded="true" @onClick="onClick" />
-                <ToolSection :category="linksSection" :expanded="false" @onClick="onClick" />
-                <ToolSection :category="otherSection" :expanded="true" @onClick="onClick" />
-                <ToolSection
-                    v-if="hasVisualizations"
-                    :category="visualizationSection"
-                    :expanded="true"
-                    @onClick="onClick" />
-            </div>
+    <ActivityPanel title="Insert Markdown Objects">
+        <div class="toolMenuContainer">
+            <b-alert v-if="error" variant="danger" class="my-2 mx-3 px-2 py-1" show>
+                {{ error }}
+            </b-alert>
+            <ToolSection v-if="isWorkflow" :category="historyInEditorSection" :expanded="true" @onClick="onClick" />
+            <ToolSection v-else :category="historySection" :expanded="true" @onClick="onClick" />
+            <ToolSection :category="jobSection" :expanded="true" @onClick="onClick" />
+            <ToolSection v-if="isWorkflow" :category="workflowInEditorSection" :expanded="true" @onClick="onClick" />
+            <ToolSection v-else :category="workflowSection" :expanded="true" @onClick="onClick" />
+            <ToolSection :category="linksSection" :expanded="false" @onClick="onClick" />
+            <ToolSection :category="otherSection" :expanded="true" @onClick="onClick" />
+            <ToolSection
+                v-if="hasVisualizations"
+                :category="visualizationSection"
+                :expanded="true"
+                @onClick="onClick" />
         </div>
         <MarkdownDialog
             v-if="selectedShow"
@@ -37,7 +26,7 @@
             :use-labels="isWorkflow"
             @onInsert="onInsert"
             @onCancel="onCancel" />
-    </div>
+    </ActivityPanel>
 </template>
 
 <script>
@@ -51,6 +40,8 @@ import { directiveEntry } from "./directives.ts";
 import { fromSteps } from "./labels.ts";
 import MarkdownDialog from "./MarkdownDialog";
 
+import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
+
 Vue.use(BootstrapVue);
 
 function historySharedElements(mode) {
@@ -95,6 +86,7 @@ export default {
     components: {
         MarkdownDialog,
         ToolSection,
+        ActivityPanel,
     },
     props: {
         steps: {
@@ -317,7 +309,7 @@ export default {
             }
         },
         onInsert(markdownBlock) {
-            this.$emit("onInsert", markdownBlock);
+            this.$emit("insert", markdownBlock);
             this.selectedShow = false;
         },
         onCancel() {
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index b5bade5a9b0b..273d6028c318 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -1,5 +1,5 @@
 <template>
-    <div v-if="isCanvas" id="columns" class="d-flex">
+    <div id="columns" class="d-flex">
         <StateUpgradeModal :state-messages="stateMessages" />
         <StateUpgradeModal
             :state-messages="insertedStateMessages"
@@ -28,6 +28,7 @@
             </b-form-group>
         </b-modal>
         <ActivityBar
+            ref="activityBar"
             :default-activities="workflowEditorActivities"
             :special-activities="specialWorkflowActivities"
             activity-bar-id="workflow-editor"
@@ -79,121 +80,125 @@
                     @creator="onCreator"
                     @update:nameCurrent="setName"
                     @update:annotationCurrent="setAnnotation" />
+                <MarkdownToolBox v-else-if="isActiveSideBar('workflow-editor-report')" @insert="insertMarkdown" />
             </template>
         </ActivityBar>
-        <div id="center" class="workflow-center">
-            <div class="editor-top-bar" unselectable="on">
-                <span>
-                    <span class="sr-only">Workflow Editor</span>
-                    <span class="editor-title" :title="name"
-                        >{{ name }}
-                        <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
-                    </span>
-                </span>
-
-                <b-button-group>
-                    <b-button
-                        :title="undoRedoStore.undoText + ' (Ctrl + Z)'"
-                        :variant="undoRedoStore.hasUndo ? 'secondary' : 'muted'"
-                        @click="undoRedoStore.undo()">
-                        <FontAwesomeIcon icon="fa-arrow-left" />
-                    </b-button>
+        <template v-if="reportActive">
+            <MarkdownEditor
+                ref="markdownEditor"
+                :markdown-text="report.markdown"
+                mode="report"
+                :title="'Workflow Report: ' + name"
+                :steps="steps"
+                @update="onReportUpdate">
+                <template v-slot:buttons>
                     <b-button
-                        :title="undoRedoStore.redoText + ' (Ctrl + Shift + Z)'"
-                        :variant="undoRedoStore.hasRedo ? 'secondary' : 'muted'"
-                        @click="undoRedoStore.redo()">
-                        <FontAwesomeIcon icon="fa-arrow-right" />
+                        id="workflow-canvas-button"
+                        v-b-tooltip.hover.bottom
+                        title="Return to Workflow"
+                        variant="link"
+                        role="button"
+                        @click="showAttributes">
+                        <FontAwesomeIcon :icon="faTimes" />
                     </b-button>
-                </b-button-group>
+                </template>
+            </MarkdownEditor>
+        </template>
+        <template v-else>
+            <div id="center" class="workflow-center">
+                <div class="editor-top-bar" unselectable="on">
+                    <span>
+                        <span class="sr-only">Workflow Editor</span>
+                        <span class="editor-title" :title="name"
+                            >{{ name }}
+                            <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
+                        </span>
+                    </span>
+
+                    <b-button-group>
+                        <b-button
+                            :title="undoRedoStore.undoText + ' (Ctrl + Z)'"
+                            :variant="undoRedoStore.hasUndo ? 'secondary' : 'muted'"
+                            @click="undoRedoStore.undo()">
+                            <FontAwesomeIcon icon="fa-arrow-left" />
+                        </b-button>
+                        <b-button
+                            :title="undoRedoStore.redoText + ' (Ctrl + Shift + Z)'"
+                            :variant="undoRedoStore.hasRedo ? 'secondary' : 'muted'"
+                            @click="undoRedoStore.redo()">
+                            <FontAwesomeIcon icon="fa-arrow-right" />
+                        </b-button>
+                    </b-button-group>
+                </div>
+                <WorkflowGraph
+                    v-if="!datatypesMapperLoading"
+                    :steps="steps"
+                    :datatypes-mapper="datatypesMapper"
+                    :highlight-id="highlightId"
+                    :scroll-to-id="scrollToId"
+                    @scrollTo="scrollToId = null"
+                    @transform="(value) => (transform = value)"
+                    @graph-offset="(value) => (graphOffset = value)"
+                    @onClone="onClone"
+                    @onCreate="onInsertTool"
+                    @onChange="onChange"
+                    @onRemove="onRemove"
+                    @onUpdateStepPosition="onUpdateStepPosition">
+                </WorkflowGraph>
             </div>
-            <WorkflowGraph
-                v-if="!datatypesMapperLoading"
-                :steps="steps"
-                :datatypes-mapper="datatypesMapper"
-                :highlight-id="highlightId"
-                :scroll-to-id="scrollToId"
-                @scrollTo="scrollToId = null"
-                @transform="(value) => (transform = value)"
-                @graph-offset="(value) => (graphOffset = value)"
-                @onClone="onClone"
-                @onCreate="onInsertTool"
-                @onChange="onChange"
-                @onRemove="onRemove"
-                @onUpdateStepPosition="onUpdateStepPosition">
-            </WorkflowGraph>
-        </div>
-        <FlexPanel side="right">
-            <div class="unified-panel bg-white">
-                <div class="unified-panel-header" unselectable="on">
-                    <div class="unified-panel-header-inner">
-                        <WorkflowOptions
-                            :is-new-temp-workflow="isNewTempWorkflow"
-                            :has-changes="hasChanges"
-                            :has-invalid-connections="hasInvalidConnections"
-                            :current-active-panel="showInPanel"
-                            @onSave="onSave"
-                            @onCreate="onCreate"
-                            @onSaveAs="onSaveAs"
-                            @onRun="onRun"
-                            @onDownload="onDownload"
-                            @onReport="onReport"
-                            @onLayout="onLayout"
-                            @onEdit="onEdit"
-                            @onAttributes="showAttributes"
-                            @onUpgrade="onUpgrade" />
+            <FlexPanel side="right">
+                <div class="unified-panel bg-white">
+                    <div class="unified-panel-header" unselectable="on">
+                        <div class="unified-panel-header-inner">
+                            <WorkflowOptions
+                                :is-new-temp-workflow="isNewTempWorkflow"
+                                :has-changes="hasChanges"
+                                :has-invalid-connections="hasInvalidConnections"
+                                @onSave="onSave"
+                                @onCreate="onCreate"
+                                @onSaveAs="onSaveAs"
+                                @onRun="onRun"
+                                @onDownload="onDownload"
+                                @onReport="onReport"
+                                @onLayout="onLayout"
+                                @onEdit="onEdit"
+                                @onAttributes="showAttributes"
+                                @onUpgrade="onUpgrade" />
+                        </div>
                     </div>
-                </div>
-                <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
-                    <div v-if="!initialLoading" class="position-relative h-100">
-                        <FormTool
-                            v-if="hasActiveNodeTool"
-                            :key="activeStep.id"
-                            :step="activeStep"
-                            :datatypes="datatypes"
-                            @onChangePostJobActions="onChangePostJobActions"
-                            @onAnnotation="onAnnotation"
-                            @onLabel="onLabel"
-                            @onSetData="onSetData"
-                            @onUpdateStep="updateStep" />
-                        <FormDefault
-                            v-else-if="hasActiveNodeDefault"
-                            :step="activeStep"
-                            :datatypes="datatypes"
-                            @onAnnotation="onAnnotation"
-                            @onLabel="onLabel"
-                            @onEditSubworkflow="onEditSubworkflow"
-                            @onAttemptRefactor="onAttemptRefactor"
-                            @onSetData="onSetData"
-                            @onUpdateStep="updateStep" />
+                    <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
+                        <div v-if="!initialLoading" class="position-relative h-100">
+                            <FormTool
+                                v-if="hasActiveNodeTool"
+                                :key="activeStep.id"
+                                :step="activeStep"
+                                :datatypes="datatypes"
+                                @onChangePostJobActions="onChangePostJobActions"
+                                @onAnnotation="onAnnotation"
+                                @onLabel="onLabel"
+                                @onSetData="onSetData"
+                                @onUpdateStep="updateStep" />
+                            <FormDefault
+                                v-else-if="hasActiveNodeDefault"
+                                :step="activeStep"
+                                :datatypes="datatypes"
+                                @onAnnotation="onAnnotation"
+                                @onLabel="onLabel"
+                                @onEditSubworkflow="onEditSubworkflow"
+                                @onAttemptRefactor="onAttemptRefactor"
+                                @onSetData="onSetData"
+                                @onUpdateStep="updateStep" />
+                        </div>
                     </div>
                 </div>
-            </div>
-        </FlexPanel>
-    </div>
-    <MarkdownEditor
-        v-else
-        :markdown-text="report.markdown"
-        mode="report"
-        :title="'Workflow Report: ' + name"
-        :steps="steps"
-        @onUpdate="onReportUpdate">
-        <template v-slot:buttons>
-            <b-button
-                id="workflow-canvas-button"
-                v-b-tooltip.hover.bottom
-                title="Return to Workflow"
-                variant="link"
-                role="button"
-                @click="onEdit">
-                <span class="fa fa-times" />
-            </b-button>
+            </FlexPanel>
         </template>
-    </MarkdownEditor>
+    </div>
 </template>
 
 <script>
 import { library } from "@fortawesome/fontawesome-svg-core";
-import { faArrowLeft, faArrowRight, faHistory } from "@fortawesome/free-solid-svg-icons";
+import { faArrowLeft, faArrowRight, faHistory, faTimes } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { whenever } from "@vueuse/core";
 import { logicAnd, logicNot, logicOr } from "@vueuse/math";
@@ -233,6 +238,7 @@ import WorkflowAttributes from "./WorkflowAttributes.vue";
 import WorkflowGraph from "./WorkflowGraph.vue";
 import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
+import MarkdownToolBox from "@/components/Markdown/MarkdownToolBox.vue";
 import FlexPanel from "@/components/Panels/FlexPanel.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
 import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue";
@@ -261,6 +267,7 @@ export default {
         FontAwesomeIcon,
         UndoRedoStack,
         WorkflowPanel,
+        MarkdownToolBox,
     },
     props: {
         workflowId: {
@@ -305,7 +312,8 @@ export default {
         whenever(logicAnd(undoKeys, logicNot(redoKeys)), undo);
         whenever(redoKeys, redo);
 
-        const isCanvas = ref(true);
+        const activityBar = ref(null);
+        const reportActive = computed(() => activityBar.value?.isActiveSideBar("workflow-editor-report"));
 
         const parameters = ref(null);
 
@@ -313,12 +321,10 @@ export default {
             parameters.value = getUntypedWorkflowParameters(steps.value);
         }
 
-        const showInPanel = ref("attributes");
-
         function showAttributes() {
             ensureParametersSet();
             stateStore.activeNodeId = null;
-            showInPanel.value = "attributes";
+            activityBar.value?.setActiveSideBar("workflow-editor-attributes");
         }
 
         const name = ref("Unnamed Workflow");
@@ -457,13 +463,16 @@ export default {
 
         const stepActions = useStepActions(stepStore, undoRedoStore, stateStore, connectionStore);
 
+        const markdownEditor = ref(null);
+        function insertMarkdown(markdown) {
+            markdownEditor.value?.insertMarkdown(markdown);
+        }
+
         return {
             id,
             name,
-            isCanvas,
             parameters,
             ensureParametersSet,
-            showInPanel,
             showAttributes,
             setName,
             report,
@@ -494,6 +503,10 @@ export default {
             initialLoading,
             stepActions,
             undoRedoStore,
+            activityBar,
+            reportActive,
+            markdownEditor,
+            insertMarkdown,
         };
     },
     data() {
@@ -520,6 +533,7 @@ export default {
             navUrl: "",
             workflowEditorActivities,
             specialWorkflowActivities,
+            faTimes,
         };
     },
     computed: {
@@ -674,10 +688,6 @@ export default {
             });
         },
         async onInsertWorkflowSteps(workflowId, stepCount) {
-            if (!this.isCanvas) {
-                this.isCanvas = true;
-                return;
-            }
             if (stepCount < 10) {
                 this.copyIntoWorkflow(workflowId);
             } else {
@@ -811,12 +821,6 @@ export default {
         onUpgrade() {
             this.onAttemptRefactor([{ action_type: "upgrade_all_steps" }]);
         },
-        onEdit() {
-            this.isCanvas = true;
-        },
-        onReport() {
-            this.isCanvas = false;
-        },
         onReportUpdate(markdown) {
             this.hasChanges = true;
             this.report.markdown = markdown;
@@ -879,11 +883,6 @@ export default {
             }
         },
         _insertStep(contentId, name, type) {
-            if (!this.isCanvas) {
-                this.isCanvas = true;
-                return;
-            }
-
             const action = new InsertStepAction(this.stepStore, this.stateStore, {
                 contentId,
                 name,
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index ee69a02b40d8..129263c0dcec 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,10 +1,10 @@
 import {
+    faEdit,
     faHistory,
     faMagic,
     faPencilAlt,
     faSave,
     faSitemap,
-    faTools,
     faWrench,
 } from "@fortawesome/free-solid-svg-icons";
 
@@ -29,16 +29,6 @@ export const workflowEditorActivities = [
         tooltip: "Search tools to use in your workflow",
         visible: true,
     },
-    {
-        title: "Workflow Tools",
-        id: "workflow-editor-utility-tools",
-        description: "Browse and insert tools specific to workflows.",
-        tooltip: "Workflow specific tools",
-        icon: faTools,
-        panel: true,
-        optional: true,
-        visible: false,
-    },
     {
         title: "Workflows",
         id: "workflow-editor-workflows",
@@ -49,6 +39,16 @@ export const workflowEditorActivities = [
         visible: true,
         optional: true,
     },
+    {
+        title: "Report",
+        id: "workflow-editor-report",
+        description: "Edit the report for this workflow.",
+        tooltip: "Edit workflow report",
+        icon: faEdit,
+        panel: true,
+        visible: true,
+        optional: true,
+    },
     {
         title: "Best Practices",
         id: "workflow-best-practices",

From 8391148c6e81684dc6cef04166c1ef7acb27be30 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 10 Sep 2024 13:54:20 +0200
Subject: [PATCH 022/131] move all options to activity bar

---
 .../components/ActivityBar/ActivityBar.vue    | 25 ++++-
 .../src/components/Panels/SettingsPanel.vue   |  6 +-
 .../src/components/Workflow/Editor/Index.vue  | 14 ++-
 .../Workflow/Editor/modules/activities.ts     | 95 +++++++++++++++++--
 4 files changed, 123 insertions(+), 17 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index ea059f38813a..51a894246137 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { faEllipsisH, type IconDefinition } from "@fortawesome/free-solid-svg-icons";
 import { watchImmediate } from "@vueuse/core";
 import { storeToRefs } from "pinia";
 import { computed, type Ref, ref } from "vue";
@@ -30,12 +31,22 @@ const props = withDefaults(
         activityBarId?: string;
         specialActivities?: Activity[];
         showAdmin?: boolean;
+        optionsTitle?: string;
+        optionsTooltip?: string;
+        optionsHeading?: string;
+        optionsIcon?: IconDefinition;
+        optionsSearchPlaceholder?: string;
     }>(),
     {
         defaultActivities: undefined,
         activityBarId: "default",
         specialActivities: () => [],
         showAdmin: true,
+        optionsTitle: "More",
+        optionsHeading: "Additional Activities",
+        optionsIcon: () => faEllipsisH,
+        optionsSearchPlaceholder: "Search Activities",
+        optionsTooltip: "View additional activities",
     }
 );
 
@@ -180,6 +191,8 @@ defineExpose({
 
 <template>
     <div class="d-flex">
+        <!-- while this warning is correct, it is hiding too many other errors -->
+        <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
         <div
             class="activity-bar d-flex flex-column no-highlight"
             data-description="activity bar"
@@ -250,10 +263,10 @@ defineExpose({
                     @click="toggleSidebar('notifications')" />
                 <ActivityItem
                     id="activity-settings"
-                    icon="ellipsis-h"
+                    :icon="props.optionsIcon"
                     :is-active="isActiveSideBar('settings')"
-                    title="More"
-                    tooltip="View additional activities"
+                    :title="props.optionsTitle"
+                    :tooltip="props.optionsTooltip"
                     @click="toggleSidebar('settings')" />
                 <ActivityItem
                     v-if="isAdmin && showAdmin"
@@ -294,7 +307,11 @@ defineExpose({
             <VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
             <MultiviewPanel v-else-if="isActiveSideBar('multiview')" />
             <NotificationsPanel v-else-if="isActiveSideBar('notifications')" />
-            <SettingsPanel v-else-if="isActiveSideBar('settings')" :activity-bar-id="props.activityBarId" />
+            <SettingsPanel
+                v-else-if="isActiveSideBar('settings')"
+                :activity-bar-id="props.activityBarId"
+                :heading="props.optionsHeading"
+                :search-placeholder="props.optionsSearchPlaceholder" />
             <AdminPanel v-else-if="isActiveSideBar('admin')" />
             <slot name="side-panel" :is-active-side-bar="isActiveSideBar"></slot>
         </FlexPanel>
diff --git a/client/src/components/Panels/SettingsPanel.vue b/client/src/components/Panels/SettingsPanel.vue
index c9f073cf0bc8..5e9b6425e9ed 100644
--- a/client/src/components/Panels/SettingsPanel.vue
+++ b/client/src/components/Panels/SettingsPanel.vue
@@ -12,6 +12,8 @@ import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
 
 const props = defineProps<{
     activityBarId: string;
+    heading: string;
+    searchPlaceholder: string;
 }>();
 
 const activityStore = useActivityStore(props.activityBarId);
@@ -25,9 +27,9 @@ function onQuery(newQuery: string) {
 </script>
 
 <template>
-    <ActivityPanel title="Additional Activities">
+    <ActivityPanel :title="props.heading">
         <template v-slot:header>
-            <DelayedInput :delay="100" placeholder="Search activities" @change="onQuery" />
+            <DelayedInput :delay="100" :placeholder="props.searchPlaceholder" @change="onQuery" />
         </template>
         <template v-slot:header-buttons>
             <BButton
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 273d6028c318..3c28658e2708 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -33,6 +33,11 @@
             :special-activities="specialWorkflowActivities"
             activity-bar-id="workflow-editor"
             :show-admin="false"
+            options-title="Options"
+            options-heading="Workflow Options"
+            options-tooltip="View additional workflow options"
+            options-search-placeholder="Search options"
+            :options-icon="faCog"
             @activityClicked="onActivityClicked">
             <template v-slot:side-panel="{ isActiveSideBar }">
                 <ToolPanel
@@ -198,7 +203,7 @@
 
 <script>
 import { library } from "@fortawesome/fontawesome-svg-core";
-import { faArrowLeft, faArrowRight, faHistory, faTimes } from "@fortawesome/free-solid-svg-icons";
+import { faArrowLeft, faArrowRight, faCog, faHistory, faTimes } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { whenever } from "@vueuse/core";
 import { logicAnd, logicNot, logicOr } from "@vueuse/math";
@@ -222,7 +227,7 @@ import { Services } from "../services";
 import { InsertStepAction, useStepActions } from "./Actions/stepActions";
 import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
 import { defaultPosition } from "./composables/useDefaultStepPosition";
-import { specialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
+import { useSpecialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
 import { fromSimple } from "./modules/model";
 import { getModule, getVersions, loadWorkflow, saveWorkflow } from "./modules/services";
 import { getStateUpgradeMessages } from "./modules/utilities";
@@ -468,6 +473,8 @@ export default {
             markdownEditor.value?.insertMarkdown(markdown);
         }
 
+        const { specialWorkflowActivities } = useSpecialWorkflowActivities();
+
         return {
             id,
             name,
@@ -507,6 +514,7 @@ export default {
             reportActive,
             markdownEditor,
             insertMarkdown,
+            specialWorkflowActivities,
         };
     },
     data() {
@@ -532,8 +540,8 @@ export default {
             showSaveChangesModal: false,
             navUrl: "",
             workflowEditorActivities,
-            specialWorkflowActivities,
             faTimes,
+            faCog,
         };
     },
     computed: {
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index 129263c0dcec..5a17e951d2df 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,12 +1,19 @@
+import { faSave as farSave } from "@fortawesome/free-regular-svg-icons";
 import {
+    faAlignLeft,
+    faDownload,
     faEdit,
     faHistory,
     faMagic,
     faPencilAlt,
+    faPlay,
+    faRecycle,
     faSave,
+    faSignOutAlt,
     faSitemap,
     faWrench,
 } from "@fortawesome/free-solid-svg-icons";
+import { ref } from "vue";
 
 import type { Activity } from "@/stores/activityStore";
 
@@ -69,20 +76,92 @@ export const workflowEditorActivities = [
         visible: true,
         optional: true,
     },
-] as const satisfies Readonly<Activity[]>;
-
-export const specialWorkflowActivities = [
+    {
+        title: "Run",
+        id: "workflow-run",
+        description: "Run this workflow with specific parameters.",
+        tooltip: "Run workflow",
+        icon: faPlay,
+        visible: true,
+        panel: true,
+        optional: true,
+    },
+    {
+        description: "Save this workflow with a different name and annotation",
+        icon: farSave,
+        id: "save-workflow-as",
+        title: "Save as",
+        tooltip: "Save a copy of this workflow",
+        visible: false,
+        click: true,
+        optional: true,
+    },
+    {
+        title: "Auto Layout",
+        id: "workflow-auto-layout",
+        description: "Automatically align the nodes in this workflow.",
+        tooltip: "Automatically align nodes",
+        icon: faAlignLeft,
+        visible: false,
+        click: true,
+        optional: true,
+    },
+    {
+        title: "Upgrade",
+        id: "workflow-upgrade",
+        description: "Update all tools used in this workflow.",
+        tooltip: "Update all tools",
+        icon: faRecycle,
+        visible: false,
+        click: true,
+        optional: true,
+    },
+    {
+        title: "Download",
+        id: "workflow-download",
+        description: "Download this workflow in '.ga' format.",
+        tooltip: "Download workflow",
+        icon: faDownload,
+        visible: false,
+        click: true,
+        optional: true,
+    },
     {
         description: "Save this workflow, then exit the workflow editor.",
         icon: faSave,
         id: "save-and-exit",
         title: "Save + Exit",
-        to: null,
         tooltip: "Save and Exit",
-        mutable: false,
-        optional: false,
-        panel: false,
-        visible: true,
+        visible: false,
         click: true,
+        optional: true,
+    },
+    {
+        description: "Exit the workflow editor and return to the start screen.",
+        icon: faSignOutAlt,
+        id: "exit",
+        title: "Exit",
+        tooltip: "Exit workflow editor",
+        visible: false,
+        click: true,
+        optional: true,
     },
 ] as const satisfies Readonly<Activity[]>;
+
+export function useSpecialWorkflowActivities() {
+    const specialWorkflowActivities = ref<Activity[]>([
+        {
+            title: "Save",
+            tooltip: "Save workflow",
+            description: "Save changes made to this workflow.",
+            icon: faSave,
+            id: "save-workflow",
+            click: true,
+            mutable: false,
+        },
+    ]);
+
+    return {
+        specialWorkflowActivities,
+    };
+}

From 7592958f380680eaeedf7c78c11681563dba3bca Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 23 Sep 2024 10:15:29 +0200
Subject: [PATCH 023/131] add save button

---
 .../components/ActivityBar/ActivityBar.vue    |  6 +-
 .../components/ActivityBar/ActivityItem.vue   | 88 +++++++++++--------
 .../ActivityBar/ActivitySettings.vue          | 25 +++++-
 .../src/components/Panels/SettingsPanel.vue   |  9 +-
 .../src/components/Workflow/Editor/Index.vue  | 30 ++++++-
 .../Workflow/Editor/modules/activities.ts     | 27 +++++-
 client/src/composables/useActivityAction.ts   | 20 -----
 client/src/stores/activityStore.ts            |  3 +
 8 files changed, 136 insertions(+), 72 deletions(-)
 delete mode 100644 client/src/composables/useActivityAction.ts

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 51a894246137..1c699ea42e78 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -248,6 +248,7 @@ defineExpose({
                                 :title="activity.title"
                                 :tooltip="activity.tooltip"
                                 :to="activity.to ?? undefined"
+                                :variant="activity.variant"
                                 @click="onActivityClicked(activity)" />
                         </div>
                     </div>
@@ -287,6 +288,7 @@ defineExpose({
                         :title="activity.title"
                         :tooltip="activity.tooltip"
                         :to="activity.to || ''"
+                        :variant="activity.variant"
                         @click="toggleSidebar(activity.id, activity.to)" />
                     <ActivityItem
                         v-else
@@ -297,6 +299,7 @@ defineExpose({
                         :title="activity.title"
                         :tooltip="activity.tooltip"
                         :to="activity.to ?? undefined"
+                        :variant="activity.variant"
                         @click="onActivityClicked(activity)" />
                 </template>
             </b-nav>
@@ -311,7 +314,8 @@ defineExpose({
                 v-else-if="isActiveSideBar('settings')"
                 :activity-bar-id="props.activityBarId"
                 :heading="props.optionsHeading"
-                :search-placeholder="props.optionsSearchPlaceholder" />
+                :search-placeholder="props.optionsSearchPlaceholder"
+                @activityClicked="(id) => emit('activityClicked', id)" />
             <AdminPanel v-else-if="isActiveSideBar('admin')" />
             <slot name="side-panel" :is-active-side-bar="isActiveSideBar"></slot>
         </FlexPanel>
diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index 0586a1657f99..278ca8d92bd1 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -1,7 +1,11 @@
 <script setup lang="ts">
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+import type { Placement } from "@popperjs/core";
 import { useRouter } from "vue-router/composables";
 
+import type { ActivityVariant } from "@/stores/activityStore";
+import localize from "@/utils/localization";
+
 import TextShort from "@/components/Common/TextShort.vue";
 import Popper from "@/components/Popper/Popper.vue";
 
@@ -19,12 +23,12 @@ export interface Props {
     indicator?: number;
     isActive?: boolean;
     tooltip?: string;
-    tooltipPlacement?: string;
+    tooltipPlacement?: Placement;
     progressPercentage?: number;
     progressStatus?: string;
     options?: Option[];
     to?: string;
-    variant?: string;
+    variant?: ActivityVariant;
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -56,36 +60,36 @@ function onClick(evt: MouseEvent): void {
 <template>
     <Popper reference-is="span" popper-is="span" :placement="tooltipPlacement">
         <template v-slot:reference>
-            <div :id="id" class="activity-item" @click="onClick">
-                <b-nav-item
-                    class="position-relative my-1 p-2"
-                    :class="{ 'nav-item-active': isActive }"
-                    :aria-label="title | l">
-                    <span v-if="progressStatus" class="progress">
-                        <div
-                            class="progress-bar notransition"
-                            :class="{
-                                'bg-danger': progressStatus === 'danger',
-                                'bg-success': progressStatus === 'success',
-                            }"
-                            :style="{
-                                width: `${Math.round(progressPercentage)}%`,
-                            }" />
-                    </span>
-                    <span class="position-relative" :class="`text-${variant}`">
-                        <div class="nav-icon">
-                            <span v-if="indicator > 0" class="nav-indicator" data-description="activity indicator">
-                                {{ Math.min(indicator, 99) }}
-                            </span>
-                            <FontAwesomeIcon :icon="icon" />
-                        </div>
-                        <TextShort v-if="title" :text="title" class="nav-title" />
-                    </span>
-                </b-nav-item>
-            </div>
+            <b-nav-item
+                class="activity-item position-relative my-1 p-2"
+                :class="{ 'nav-item-active': isActive }"
+                :link-classes="`variant-${props.variant}`"
+                :aria-label="localize(title)"
+                @click="onClick">
+                <span v-if="progressStatus" class="progress">
+                    <div
+                        class="progress-bar notransition"
+                        :class="{
+                            'bg-danger': progressStatus === 'danger',
+                            'bg-success': progressStatus === 'success',
+                        }"
+                        :style="{
+                            width: `${Math.round(progressPercentage)}%`,
+                        }" />
+                </span>
+                <span class="position-relative">
+                    <div class="nav-icon">
+                        <span v-if="indicator > 0" class="nav-indicator" data-description="activity indicator">
+                            {{ Math.min(indicator, 99) }}
+                        </span>
+                        <FontAwesomeIcon :icon="icon" />
+                    </div>
+                    <TextShort v-if="title" :text="title" class="nav-title" />
+                </span>
+            </b-nav-item>
         </template>
         <div class="text-center px-2 py-1">
-            <small v-if="tooltip">{{ tooltip | l }}</small>
+            <small v-if="tooltip">{{ localize(tooltip) }}</small>
             <small v-else>No tooltip available for this item</small>
             <div v-if="options" class="nav-options p-1">
                 <router-link v-for="(option, index) in options" :key="index" :to="option.value">
@@ -101,8 +105,20 @@ function onClick(evt: MouseEvent): void {
 <style scoped lang="scss">
 @import "theme/blue.scss";
 
+.activity-item {
+    &:deep(.variant-danger) {
+        color: $brand-danger;
+    }
+
+    &:deep(.variant-disabled) {
+        color: $text-light;
+    }
+}
+
 .nav-icon {
-    @extend .nav-item;
+    display: flex;
+    justify-content: center;
+    cursor: pointer;
     font-size: 1rem;
 }
 
@@ -121,13 +137,6 @@ function onClick(evt: MouseEvent): void {
     width: 1.2rem;
 }
 
-.nav-item {
-    display: flex;
-    align-items: center;
-    align-content: center;
-    justify-content: center;
-}
-
 .nav-item-active {
     border-radius: $border-radius-extralarge;
     background: $gray-300;
@@ -143,7 +152,8 @@ function onClick(evt: MouseEvent): void {
 }
 
 .nav-title {
-    @extend .nav-item;
+    display: flex;
+    justify-content: center;
     width: 4rem;
     margin-top: 0.5rem;
     font-size: 0.7rem;
diff --git a/client/src/components/ActivityBar/ActivitySettings.vue b/client/src/components/ActivityBar/ActivitySettings.vue
index 7f55a5fc59e9..36aec9e0c220 100644
--- a/client/src/components/ActivityBar/ActivitySettings.vue
+++ b/client/src/components/ActivityBar/ActivitySettings.vue
@@ -5,8 +5,8 @@ import { faCheckSquare, faStar, faThumbtack, faTrash } from "@fortawesome/free-s
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { storeToRefs } from "pinia";
 import { computed, type ComputedRef } from "vue";
+import { useRouter } from "vue-router/composables";
 
-import { useActivityAction } from "@/composables/useActivityAction";
 import { type Activity, useActivityStore } from "@/stores/activityStore";
 
 library.add({
@@ -23,9 +23,12 @@ const props = defineProps<{
     query: string;
 }>();
 
+const emit = defineEmits<{
+    (e: "activityClicked", activityId: string): void;
+}>();
+
 const activityStore = useActivityStore(props.activityBarId);
 const { activities } = storeToRefs(activityStore);
-const activityAction = useActivityAction(props.activityBarId);
 
 const filteredActivities = computed(() => {
     if (props.query?.length > 0) {
@@ -58,6 +61,22 @@ function onFavorite(activity: Activity) {
 function onRemove(activity: Activity) {
     activityStore.remove(activity.id);
 }
+
+const router = useRouter();
+
+function executeActivity(activity: Activity) {
+    if (activity.click) {
+        emit("activityClicked", activity.id);
+    }
+
+    if (activity.panel) {
+        activityStore.toggleSideBar(activity.id);
+    }
+
+    if (activity.to) {
+        router.push(activity.to);
+    }
+}
 </script>
 
 <template>
@@ -67,7 +86,7 @@ function onRemove(activity: Activity) {
                 <button
                     v-if="activity.optional"
                     class="activity-settings-item p-2 cursor-pointer"
-                    @click="activityAction.executeActivity(activity)">
+                    @click="executeActivity(activity)">
                     <div class="d-flex justify-content-between align-items-start">
                         <span class="d-flex justify-content-between w-100">
                             <span>
diff --git a/client/src/components/Panels/SettingsPanel.vue b/client/src/components/Panels/SettingsPanel.vue
index 5e9b6425e9ed..7ffe6dbf5cd4 100644
--- a/client/src/components/Panels/SettingsPanel.vue
+++ b/client/src/components/Panels/SettingsPanel.vue
@@ -16,6 +16,10 @@ const props = defineProps<{
     searchPlaceholder: string;
 }>();
 
+const emit = defineEmits<{
+    (e: "activityClicked", activityId: string): void;
+}>();
+
 const activityStore = useActivityStore(props.activityBarId);
 
 const confirmRestore = ref(false);
@@ -43,7 +47,10 @@ function onQuery(newQuery: string) {
                 <FontAwesomeIcon :icon="faUndo" fixed-width />
             </BButton>
         </template>
-        <ActivitySettings :query="query" :activity-bar-id="props.activityBarId" />
+        <ActivitySettings
+            :query="query"
+            :activity-bar-id="props.activityBarId"
+            @activityClicked="(...args) => emit('activityClicked', ...args)" />
         <BModal
             v-model="confirmRestore"
             title="Restore Activity Bar Defaults"
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 3c28658e2708..0c57f166268e 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -473,7 +473,15 @@ export default {
             markdownEditor.value?.insertMarkdown(markdown);
         }
 
-        const { specialWorkflowActivities } = useSpecialWorkflowActivities();
+        const isNewTempWorkflow = computed(() => !props.workflowId);
+
+        const { specialWorkflowActivities } = useSpecialWorkflowActivities(
+            computed(() => ({
+                hasChanges: hasChanges.value,
+                hasInvalidConnections: hasInvalidConnections.value,
+                isNewTempWorkflow: isNewTempWorkflow.value,
+            }))
+        );
 
         return {
             id,
@@ -515,6 +523,7 @@ export default {
             markdownEditor,
             insertMarkdown,
             specialWorkflowActivities,
+            isNewTempWorkflow,
         };
     },
     data() {
@@ -554,9 +563,6 @@ export default {
         hasActiveNodeTool() {
             return this.activeStep?.type == "tool";
         },
-        isNewTempWorkflow() {
-            return !this.workflowId;
-        },
     },
     watch: {
         id(newId, oldId) {
@@ -746,6 +752,22 @@ export default {
 
                 this.$router.push("/workflows/list");
             }
+
+            if (activityId === "exit") {
+                this.$router.push("/workflows/list");
+            }
+
+            if (activityId === "workflow-download") {
+                this.onDownload();
+            }
+
+            if (activityId === "workflow-upgrade") {
+                this.onUpgrade();
+            }
+
+            if (activityId === "workflow-auto-layout") {
+                this.onLayout();
+            }
         },
         onLayout() {
             return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => {
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index 5a17e951d2df..b15f6897a961 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -13,7 +13,7 @@ import {
     faSitemap,
     faWrench,
 } from "@fortawesome/free-solid-svg-icons";
-import { ref } from "vue";
+import { computed, type Ref } from "vue";
 
 import type { Activity } from "@/stores/activityStore";
 
@@ -148,16 +148,35 @@ export const workflowEditorActivities = [
     },
 ] as const satisfies Readonly<Activity[]>;
 
-export function useSpecialWorkflowActivities() {
-    const specialWorkflowActivities = ref<Activity[]>([
+interface SpecialActivityOptions {
+    isNewTempWorkflow: boolean;
+    hasChanges: boolean;
+    hasInvalidConnections: boolean;
+}
+
+export function useSpecialWorkflowActivities(options: Ref<SpecialActivityOptions>) {
+    const saveHover = computed(() => {
+        if (options.value.isNewTempWorkflow) {
+            return "Save Workflow";
+        } else if (!options.value.hasChanges) {
+            return "Workflow has no changes";
+        } else if (options.value.hasInvalidConnections) {
+            return "Workflow has invalid connections, review and remove invalid connections";
+        } else {
+            return "Save Workflow";
+        }
+    });
+
+    const specialWorkflowActivities = computed<Activity[]>(() => [
         {
             title: "Save",
-            tooltip: "Save workflow",
+            tooltip: saveHover.value,
             description: "Save changes made to this workflow.",
             icon: faSave,
             id: "save-workflow",
             click: true,
             mutable: false,
+            variant: options.value.hasChanges ? "primary" : "disabled",
         },
     ]);
 
diff --git a/client/src/composables/useActivityAction.ts b/client/src/composables/useActivityAction.ts
deleted file mode 100644
index da9db5501851..000000000000
--- a/client/src/composables/useActivityAction.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useRouter } from "vue-router/composables";
-
-import { type Activity, useActivityStore } from "@/stores/activityStore";
-
-export function useActivityAction(activityStoreId: string) {
-    const router = useRouter();
-    const activityStore = useActivityStore(activityStoreId);
-    const executeActivity = (activity: Activity) => {
-        if (activity.panel) {
-            activityStore.toggleSideBar(activity.id);
-        }
-        if (activity.to) {
-            router.push(activity.to);
-        }
-    };
-
-    return {
-        executeActivity,
-    };
-}
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index 6ee776543937..f6f4006a5577 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -10,6 +10,8 @@ import { useUserLocalStorage } from "@/composables/userLocalStorage";
 import { defaultActivities } from "./activitySetup";
 import { defineScopedStore } from "./scopedStore";
 
+export type ActivityVariant = "primary" | "danger" | "disabled";
+
 export interface Activity {
     // determine wether an anonymous user can access this activity
     anonymous?: boolean;
@@ -35,6 +37,7 @@ export interface Activity {
     visible?: boolean;
     // if activity should cause a click event
     click?: true;
+    variant?: ActivityVariant;
 }
 
 export const useActivityStore = defineScopedStore("activityStore", (scope) => {

From 4dd15a8ff28cd5bca25a66e6eced7d9d195b25c8 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 23 Sep 2024 10:32:06 +0200
Subject: [PATCH 024/131] remove unused attribute and unreachable code

---
 client/src/components/Panels/ActivityPanel.vue  | 8 +-------
 client/src/components/Panels/MultiviewPanel.vue | 6 +-----
 2 files changed, 2 insertions(+), 12 deletions(-)

diff --git a/client/src/components/Panels/ActivityPanel.vue b/client/src/components/Panels/ActivityPanel.vue
index b0349150a92b..057e09df25f2 100644
--- a/client/src/components/Panels/ActivityPanel.vue
+++ b/client/src/components/Panels/ActivityPanel.vue
@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import { BButton } from "bootstrap-vue";
 import { computed } from "vue";
-import { useRoute } from "vue-router/composables";
 
 interface Props {
     title: string;
@@ -14,16 +13,11 @@ interface Props {
 const props = withDefaults(defineProps<Props>(), {
     goToAllTitle: undefined,
     href: undefined,
-    goToOnHref: true,
 });
 
-const route = useRoute();
-
 const emit = defineEmits(["goToAll"]);
 
-const hasGoToAll = computed(
-    () => props.goToAllTitle && props.href && (props.goToOnHref || (!props.goToOnHref && route.path !== props.href))
-);
+const hasGoToAll = computed(() => props.goToAllTitle && props.href);
 </script>
 
 <template>
diff --git a/client/src/components/Panels/MultiviewPanel.vue b/client/src/components/Panels/MultiviewPanel.vue
index a71a31fda746..5f1c9dbe43cd 100644
--- a/client/src/components/Panels/MultiviewPanel.vue
+++ b/client/src/components/Panels/MultiviewPanel.vue
@@ -96,11 +96,7 @@ function userTitle(title: string) {
 </script>
 
 <template>
-    <ActivityPanel
-        title="Select Histories"
-        go-to-all-title="Open History Multiview"
-        href="/histories/view_multiple"
-        :go-to-on-href="false">
+    <ActivityPanel title="Select Histories">
         <template v-slot:header-buttons>
             <BButtonGroup>
                 <BButton

From 5806bcf328b6e8e4d07b7f8a4546507ff2019e85 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 23 Sep 2024 14:33:18 +0200
Subject: [PATCH 025/131] enable run and save

---
 .../src/components/Workflow/Editor/Index.vue  |  28 +--
 .../Workflow/Editor/Options.test.js           |  32 ----
 .../components/Workflow/Editor/Options.vue    | 167 ------------------
 .../Workflow/Editor/modules/activities.ts     |   8 +-
 4 files changed, 12 insertions(+), 223 deletions(-)
 delete mode 100644 client/src/components/Workflow/Editor/Options.test.js
 delete mode 100644 client/src/components/Workflow/Editor/Options.vue

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 0c57f166268e..41677ec9fa25 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -153,24 +153,6 @@
             </div>
             <FlexPanel side="right">
                 <div class="unified-panel bg-white">
-                    <div class="unified-panel-header" unselectable="on">
-                        <div class="unified-panel-header-inner">
-                            <WorkflowOptions
-                                :is-new-temp-workflow="isNewTempWorkflow"
-                                :has-changes="hasChanges"
-                                :has-invalid-connections="hasInvalidConnections"
-                                @onSave="onSave"
-                                @onCreate="onCreate"
-                                @onSaveAs="onSaveAs"
-                                @onRun="onRun"
-                                @onDownload="onDownload"
-                                @onReport="onReport"
-                                @onLayout="onLayout"
-                                @onEdit="onEdit"
-                                @onAttributes="showAttributes"
-                                @onUpgrade="onUpgrade" />
-                        </div>
-                    </div>
                     <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
                         <div v-if="!initialLoading" class="position-relative h-100">
                             <FormTool
@@ -235,7 +217,6 @@ import reportDefault from "./reportDefault";
 
 import WorkflowLint from "./Lint.vue";
 import MessagesModal from "./MessagesModal.vue";
-import WorkflowOptions from "./Options.vue";
 import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
 import SaveChangesModal from "./SaveChangesModal.vue";
 import StateUpgradeModal from "./StateUpgradeModal.vue";
@@ -263,7 +244,6 @@ export default {
         ToolPanel,
         FormDefault,
         FormTool,
-        WorkflowOptions,
         WorkflowAttributes,
         WorkflowLint,
         RefactorConfirmationModal,
@@ -768,6 +748,14 @@ export default {
             if (activityId === "workflow-auto-layout") {
                 this.onLayout();
             }
+
+            if (activityId === "workflow-run") {
+                this.onRun();
+            }
+
+            if (activityId === "save-workflow") {
+                this.onSave();
+            }
         },
         onLayout() {
             return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => {
diff --git a/client/src/components/Workflow/Editor/Options.test.js b/client/src/components/Workflow/Editor/Options.test.js
deleted file mode 100644
index b51974a781b0..000000000000
--- a/client/src/components/Workflow/Editor/Options.test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { shallowMount } from "@vue/test-utils";
-import { getLocalVue } from "tests/jest/helpers";
-
-import Options from "./Options";
-
-const localVue = getLocalVue();
-
-describe("Options", () => {
-    it("render properly", async () => {
-        const wrapper = shallowMount(Options, {
-            propsData: { hasChanges: true },
-            localVue,
-        });
-        await wrapper.vm.$nextTick();
-        expect(wrapper.find(".editor-button-save").attributes("role")).toBe("button");
-        expect(wrapper.find(".editor-button-save").attributes("disabled")).toBeFalsy();
-        expect(wrapper.find(".editor-button-save-group").attributes("title")).toBe("Save Workflow");
-
-        // requires a non-shallow mount
-        // wrapper.find('.editor-button-attributes').trigger('click');
-        // expect(wrapper.emitted().onAttributes).toBeTruthy();
-    });
-
-    it("should disable save if no changes", async () => {
-        const wrapper = shallowMount(Options, {
-            propsData: { hasChanges: false },
-            localVue,
-        });
-        await wrapper.vm.$nextTick();
-        expect(wrapper.find(".editor-button-save").attributes("disabled")).toBe("true");
-    });
-});
diff --git a/client/src/components/Workflow/Editor/Options.vue b/client/src/components/Workflow/Editor/Options.vue
deleted file mode 100644
index d7e395d63a07..000000000000
--- a/client/src/components/Workflow/Editor/Options.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faSave } from "@fortawesome/free-regular-svg-icons";
-import {
-    faAlignLeft,
-    faCog,
-    faDownload,
-    faEdit,
-    faHistory,
-    faPencilAlt,
-    faPlay,
-    faRecycle,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { BButton, BDropdown, BDropdownItem } from "bootstrap-vue";
-import { computed } from "vue";
-
-import { useConfirmDialog } from "@/composables/confirmDialog";
-
-library.add(faPencilAlt, faEdit, faCog, faAlignLeft, faDownload, faPlay, faHistory, faSave, faRecycle);
-
-const emit = defineEmits<{
-    (e: "onAttributes"): void;
-    (e: "onSave"): void;
-    (e: "onCreate"): void;
-    (e: "onReport"): void;
-    (e: "onSaveAs"): void;
-    (e: "onLayout"): void;
-    (e: "onUpgrade"): void;
-    (e: "onDownload"): void;
-    (e: "onRun"): void;
-}>();
-
-const props = defineProps<{
-    isNewTempWorkflow?: boolean;
-    hasChanges?: boolean;
-    hasInvalidConnections?: boolean;
-    requiredReindex?: boolean;
-}>();
-
-const { confirm } = useConfirmDialog();
-
-const saveHover = computed(() => {
-    if (props.isNewTempWorkflow) {
-        return "Save Workflow";
-    } else if (!props.hasChanges) {
-        return "Workflow has no changes";
-    } else if (props.hasInvalidConnections) {
-        return "Workflow has invalid connections, review and remove invalid connections";
-    } else {
-        return "Save Workflow";
-    }
-});
-
-function emitSaveOrCreate() {
-    if (props.isNewTempWorkflow) {
-        emit("onCreate");
-    } else {
-        emit("onSave");
-    }
-}
-
-async function onSave() {
-    if (props.hasInvalidConnections) {
-        console.log("getting confirmation");
-        const confirmed = await confirm(
-            `Workflow has invalid connections. You can save the workflow, but it may not run correctly.`,
-            {
-                id: "save-workflow-confirmation",
-                okTitle: "Save Workflow",
-            }
-        );
-        if (confirmed) {
-            emitSaveOrCreate();
-        }
-    } else {
-        emitSaveOrCreate();
-    }
-}
-</script>
-
-<template>
-    <div class="panel-header-buttons">
-        <BButton
-            id="workflow-home-button"
-            v-b-tooltip.hover.noninteractive
-            role="button"
-            title="Edit Attributes"
-            variant="link"
-            aria-label="Edit Attributes"
-            class="editor-button-attributes"
-            @click="emit('onAttributes')">
-            <FontAwesomeIcon icon="fa fa-pencil-alt" />
-        </BButton>
-
-        <b-button-group v-b-tooltip.hover.noninteractive class="editor-button-save-group" :title="saveHover">
-            <BButton
-                id="workflow-save-button"
-                role="button"
-                :variant="isNewTempWorkflow ? 'primary' : 'link'"
-                aria-label="Save Workflow"
-                class="editor-button-save"
-                :disabled="!isNewTempWorkflow && !hasChanges"
-                @click="onSave">
-                <FontAwesomeIcon icon="far fa-save" />
-            </BButton>
-        </b-button-group>
-
-        <BButton
-            id="workflow-report-button"
-            v-b-tooltip.hover.noninteractive
-            role="button"
-            title="Edit Report"
-            variant="link"
-            aria-label="Edit Report"
-            class="editor-button-report"
-            :disabled="isNewTempWorkflow"
-            @click="emit('onReport')">
-            <FontAwesomeIcon icon="fa fa-edit" />
-        </BButton>
-
-        <BDropdown
-            id="workflow-options-button"
-            v-b-tooltip.hover.noninteractive
-            no-caret
-            right
-            role="button"
-            title="Workflow Options"
-            variant="link"
-            aria-label="Workflow Options"
-            class="editor-button-options"
-            :disabled="isNewTempWorkflow">
-            <template v-slot:button-content>
-                <FontAwesomeIcon icon="fa fa-cog" />
-            </template>
-
-            <BDropdownItem href="#" @click="emit('onSaveAs')">
-                <FontAwesomeIcon icon="far fa-save" /> Save As...
-            </BDropdownItem>
-
-            <BDropdownItem href="#" @click="emit('onLayout')">
-                <FontAwesomeIcon icon="fa fa-align-left" /> Auto Layout
-            </BDropdownItem>
-
-            <BDropdownItem href="#" @click="emit('onUpgrade')">
-                <FontAwesomeIcon icon="fa fa-recycle" /> Upgrade Workflow
-            </BDropdownItem>
-
-            <BDropdownItem href="#" @click="emit('onDownload')">
-                <FontAwesomeIcon icon="fa fa-download" /> Download
-            </BDropdownItem>
-        </BDropdown>
-
-        <BButton
-            id="workflow-run-button"
-            v-b-tooltip.hover.noninteractive
-            role="button"
-            title="Run Workflow"
-            variant="link"
-            aria-label="Run Workflow"
-            class="editor-button-run"
-            :disabled="isNewTempWorkflow"
-            @click="emit('onRun')">
-            <FontAwesomeIcon icon="fa fa-play" />
-        </BButton>
-    </div>
-</template>
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index b15f6897a961..e99e1c68568a 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -83,7 +83,7 @@ export const workflowEditorActivities = [
         tooltip: "Run workflow",
         icon: faPlay,
         visible: true,
-        panel: true,
+        click: true,
         optional: true,
     },
     {
@@ -102,7 +102,7 @@ export const workflowEditorActivities = [
         description: "Automatically align the nodes in this workflow.",
         tooltip: "Automatically align nodes",
         icon: faAlignLeft,
-        visible: false,
+        visible: true,
         click: true,
         optional: true,
     },
@@ -112,7 +112,7 @@ export const workflowEditorActivities = [
         description: "Update all tools used in this workflow.",
         tooltip: "Update all tools",
         icon: faRecycle,
-        visible: false,
+        visible: true,
         click: true,
         optional: true,
     },
@@ -122,7 +122,7 @@ export const workflowEditorActivities = [
         description: "Download this workflow in '.ga' format.",
         tooltip: "Download workflow",
         icon: faDownload,
-        visible: false,
+        visible: true,
         click: true,
         optional: true,
     },

From 859c80d3768a321d43e3cfa302f36e9220415d4a Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 23 Sep 2024 15:02:37 +0200
Subject: [PATCH 026/131] fix activity text centering on chrome

---
 .../components/ActivityBar/ActivityItem.vue    | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index 278ca8d92bd1..cb0bb19e1f7a 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -77,15 +77,13 @@ function onClick(evt: MouseEvent): void {
                             width: `${Math.round(progressPercentage)}%`,
                         }" />
                 </span>
-                <span class="position-relative">
-                    <div class="nav-icon">
-                        <span v-if="indicator > 0" class="nav-indicator" data-description="activity indicator">
-                            {{ Math.min(indicator, 99) }}
-                        </span>
-                        <FontAwesomeIcon :icon="icon" />
-                    </div>
-                    <TextShort v-if="title" :text="title" class="nav-title" />
-                </span>
+                <div class="nav-icon">
+                    <span v-if="indicator > 0" class="nav-indicator" data-description="activity indicator">
+                        {{ Math.min(indicator, 99) }}
+                    </span>
+                    <FontAwesomeIcon :icon="icon" />
+                </div>
+                <TextShort v-if="title" :text="title" class="nav-title" />
             </b-nav-item>
         </template>
         <div class="text-center px-2 py-1">
@@ -106,6 +104,8 @@ function onClick(evt: MouseEvent): void {
 @import "theme/blue.scss";
 
 .activity-item {
+    display: flex;
+
     &:deep(.variant-danger) {
         color: $brand-danger;
     }

From 8df1987a1b39074c458a97d70ae570543815bb04 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 24 Sep 2024 10:56:34 +0200
Subject: [PATCH 027/131] fix text short property collision

---
 client/src/components/ActivityBar/ActivityItem.vue | 1 +
 client/src/components/Common/TextShort.vue         | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index cb0bb19e1f7a..20c040dee10e 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -105,6 +105,7 @@ function onClick(evt: MouseEvent): void {
 
 .activity-item {
     display: flex;
+    flex-direction: column;
 
     &:deep(.variant-danger) {
         color: $brand-danger;
diff --git a/client/src/components/Common/TextShort.vue b/client/src/components/Common/TextShort.vue
index 012c770c6137..e1f0842a508c 100644
--- a/client/src/components/Common/TextShort.vue
+++ b/client/src/components/Common/TextShort.vue
@@ -10,7 +10,7 @@ const props = withDefaults(defineProps<Props>(), {
     maxLength: 24,
 });
 
-const text = computed(() => {
+const trimmedText = computed(() => {
     if (props.text.length > props.maxLength) {
         const partialText = props.text.slice(0, props.maxLength);
         return `${partialText}...`;
@@ -22,6 +22,6 @@ const text = computed(() => {
 
 <template>
     <span class="text-break text-center">
-        {{ text }}
+        {{ trimmedText }}
     </span>
 </template>

From 049f9dbc3957e909cbfb070f43e31f54d58b4997 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 27 Sep 2024 16:20:57 +0200
Subject: [PATCH 028/131] decouple node inspector from right hand panel

---
 .../components/Common/DraggableSeparator.vue  | 139 +++++++++++++++++
 client/src/components/Panels/FlexPanel.vue    | 143 ++----------------
 .../src/components/Workflow/Editor/Index.vue  |  13 +-
 .../Workflow/Editor/NodeInspector.vue         |  41 +++++
 .../Workflow/Editor/WorkflowGraph.vue         |   1 +
 client/src/composables/eventEmitter.ts        |  20 +++
 6 files changed, 216 insertions(+), 141 deletions(-)
 create mode 100644 client/src/components/Common/DraggableSeparator.vue
 create mode 100644 client/src/components/Workflow/Editor/NodeInspector.vue
 create mode 100644 client/src/composables/eventEmitter.ts

diff --git a/client/src/components/Common/DraggableSeparator.vue b/client/src/components/Common/DraggableSeparator.vue
new file mode 100644
index 000000000000..b722bb3c1802
--- /dev/null
+++ b/client/src/components/Common/DraggableSeparator.vue
@@ -0,0 +1,139 @@
+<script setup lang="ts">
+import { useDebounce, useDraggable } from "@vueuse/core";
+import { computed, ref, watch } from "vue";
+
+import { useEmit } from "@/composables/eventEmitter";
+import { useClamp } from "@/composables/math";
+import { useAnimationFrameThrottle } from "@/composables/throttle";
+
+const props = withDefaults(
+    defineProps<{
+        position: number;
+        min?: number;
+        max?: number;
+        showDelay?: number;
+        keyboardStepSize?: number;
+        side: "left" | "right";
+        additionalOffset?: number;
+    }>(),
+    {
+        showDelay: 100,
+        keyboardStepSize: 50,
+        min: -Infinity,
+        max: Infinity,
+        additionalOffset: 0,
+    }
+);
+
+const emit = defineEmits<{
+    (e: "positionChanged", position: number): void;
+    (e: "visibilityChanged", isVisible: boolean): void;
+    (e: "dragging", isDragging: boolean): void;
+}>();
+
+const { throttle } = useAnimationFrameThrottle();
+
+const draggable = ref<HTMLButtonElement | null>(null);
+
+const { position: draggablePosition, isDragging } = useDraggable(draggable, {
+    preventDefault: true,
+    exact: true,
+    initialValue: { x: props.position, y: 0 },
+});
+
+useEmit(isDragging, emit, "dragging");
+
+const handlePosition = useClamp(
+    ref(props.position),
+    () => props.min,
+    () => props.max
+);
+
+useEmit(handlePosition, emit, "positionChanged");
+
+watch(
+    () => props.position,
+    () => (handlePosition.value = props.position)
+);
+
+function updatePosition() {
+    handlePosition.value = draggablePosition.value.x;
+}
+
+watch(
+    () => draggablePosition.value,
+    () => throttle(updatePosition)
+);
+
+const hoverDraggable = ref(false);
+
+const hoverDraggableDebounced = useDebounce(
+    hoverDraggable,
+    computed(() => props.showDelay)
+);
+
+const showHover = computed(() => (hoverDraggable.value && hoverDraggableDebounced.value) || isDragging.value);
+
+useEmit(showHover, emit, "visibilityChanged");
+
+function onKeyLeft() {
+    handlePosition.value -= props.keyboardStepSize;
+}
+
+function onKeyRight() {
+    handlePosition.value += props.keyboardStepSize;
+}
+
+const style = computed(() => ({
+    "--position": handlePosition.value + "px",
+}));
+</script>
+
+<template>
+    <button
+        ref="draggable"
+        class="drag-handle"
+        :class="`side-${props.side}`"
+        :style="style"
+        @mouseenter="hoverDraggable = true"
+        @focusin="hoverDraggable = true"
+        @mouseout="hoverDraggable = false"
+        @focusout="hoverDraggable = false"
+        @keydown.left="onKeyLeft"
+        @keydown.right="onKeyRight">
+        <span class="sr-only"> Resizable drag handle </span>
+    </button>
+</template>
+
+<style scoped lang="scss">
+$border-width: 8px;
+
+.drag-handle {
+    --position: 0;
+
+    background-color: transparent;
+    background: none;
+    border: none;
+    border-radius: 0;
+    position: absolute;
+    width: $border-width;
+    padding: 0;
+    height: 100%;
+    z-index: 10000;
+
+    --hover-expand: 4px;
+
+    &:hover {
+        cursor: ew-resize;
+        width: calc($border-width + var(--hover-expand));
+    }
+
+    &.side-left {
+        left: var(--position);
+    }
+
+    &.side-right {
+        right: var(--position);
+    }
+}
+</style>
diff --git a/client/src/components/Panels/FlexPanel.vue b/client/src/components/Panels/FlexPanel.vue
index 9d0031af395f..a01a8612b097 100644
--- a/client/src/components/Panels/FlexPanel.vue
+++ b/client/src/components/Panels/FlexPanel.vue
@@ -2,14 +2,9 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { useDebounce, useDraggable } from "@vueuse/core";
 import { computed, ref, watch } from "vue";
 
-import { useTimeoutThrottle } from "@/composables/throttle";
-
-import { determineWidth } from "./utilities";
-
-const { throttle } = useTimeoutThrottle(10);
+import DraggableSeparator from "../Common/DraggableSeparator.vue";
 
 library.add(faChevronLeft, faChevronRight);
 
@@ -28,26 +23,18 @@ const props = withDefaults(defineProps<Props>(), {
     defaultWidth: 300,
 });
 
-const draggable = ref<HTMLElement | null>(null);
-const root = ref<HTMLElement | null>(null);
-
 const panelWidth = ref(props.defaultWidth);
-const show = ref(true);
-
-const { position, isDragging } = useDraggable(draggable, {
-    preventDefault: true,
-    exact: true,
-});
 
-const hoverDraggable = ref(false);
-const hoverDraggableDebounced = useDebounce(hoverDraggable, 100);
-const showHover = computed(() => (hoverDraggable.value && hoverDraggableDebounced.value) || isDragging.value);
+const root = ref<HTMLElement | null>(null);
+const show = ref(true);
 
 const showToggle = ref(false);
 const hoverToggle = ref(false);
-const hoverDraggableOrToggle = computed(
-    () => (hoverDraggableDebounced.value || hoverToggle.value) && !isDragging.value
-);
+
+const isHoveringDragHandle = ref(false);
+const isDragging = ref(false);
+
+const hoverDraggableOrToggle = computed(() => (isHoveringDragHandle.value || hoverToggle.value) && !isDragging.value);
 
 const toggleLinger = 500;
 const toggleShowDelay = 600;
@@ -70,72 +57,6 @@ watch(
     }
 );
 
-/** Watch position changes and adjust width accordingly */
-watch(position, () => {
-    throttle(() => {
-        if (!root.value || !draggable.value) {
-            return;
-        }
-
-        const rectRoot = root.value.getBoundingClientRect();
-        const rectDraggable = draggable.value.getBoundingClientRect();
-        panelWidth.value = determineWidth(
-            rectRoot,
-            rectDraggable,
-            props.minWidth,
-            props.maxWidth,
-            props.side,
-            position.value.x
-        );
-    });
-});
-
-/** If the `maxWidth` changes, prevent the panel from exceeding it */
-watch(
-    () => props.maxWidth,
-    (newVal) => {
-        if (newVal && panelWidth.value > newVal) {
-            panelWidth.value = props.maxWidth;
-        }
-    },
-    { immediate: true }
-);
-
-/** If the `minWidth` changes, ensure the panel width is at least the `minWidth` */
-watch(
-    () => props.minWidth,
-    (newVal) => {
-        if (newVal && panelWidth.value < newVal) {
-            panelWidth.value = newVal;
-        }
-    },
-    { immediate: true }
-);
-
-function onKeyLeft() {
-    if (props.side === "left") {
-        decreaseWidth();
-    } else {
-        increaseWidth();
-    }
-}
-
-function onKeyRight() {
-    if (props.side === "left") {
-        increaseWidth();
-    } else {
-        decreaseWidth();
-    }
-}
-
-function increaseWidth(by = 50) {
-    panelWidth.value = Math.min(panelWidth.value + by, props.maxWidth);
-}
-
-function decreaseWidth(by = 50) {
-    panelWidth.value = Math.max(panelWidth.value - by, props.minWidth);
-}
-
 const sideClasses = computed(() => ({
     left: props.side === "left",
     right: props.side === "right",
@@ -148,19 +69,9 @@ const sideClasses = computed(() => ({
         :id="side"
         ref="root"
         class="flex-panel"
-        :class="{ ...sideClasses, 'show-hover': showHover }"
+        :class="{ ...sideClasses }"
         :style="`--width: ${panelWidth}px`">
-        <button
-            ref="draggable"
-            class="drag-handle"
-            @mouseenter="hoverDraggable = true"
-            @focusin="hoverDraggable = true"
-            @mouseout="hoverDraggable = false"
-            @focusout="hoverDraggable = false"
-            @keydown.left="onKeyLeft"
-            @keydown.right="onKeyRight">
-            <span class="sr-only"> Side panel drag handle </span>
-        </button>
+        <DraggableSeparator :position="panelWidth" :side="props.side"></DraggableSeparator>
 
         <button
             v-if="props.collapsible"
@@ -235,14 +146,6 @@ $border-width: 6px;
         &::after {
             right: -1px;
         }
-
-        .drag-handle {
-            right: -$border-width;
-
-            &:hover {
-                right: calc(-1 * $border-width - var(--hover-expand) / 2);
-            }
-        }
     }
 
     &.right {
@@ -251,32 +154,6 @@ $border-width: 6px;
         &::after {
             left: -1px;
         }
-
-        .drag-handle {
-            left: -$border-width;
-
-            &:hover {
-                left: calc(-1 * $border-width - var(--hover-expand) / 2);
-            }
-        }
-    }
-}
-
-.drag-handle {
-    background: none;
-    border: none;
-    border-radius: 0;
-    position: absolute;
-    width: $border-width;
-    padding: 0;
-    height: 100%;
-    z-index: 10000;
-
-    --hover-expand: 4px;
-
-    &:hover {
-        cursor: ew-resize;
-        width: calc($border-width + var(--hover-expand));
     }
 }
 
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 41677ec9fa25..76164c234b3f 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -149,9 +149,10 @@
                     @onChange="onChange"
                     @onRemove="onRemove"
                     @onUpdateStepPosition="onUpdateStepPosition">
+                    <NodeInspector v-if="activeStep" :step="activeStep"></NodeInspector>
                 </WorkflowGraph>
             </div>
-            <FlexPanel side="right">
+            <!--FlexPanel side="right">
                 <div class="unified-panel bg-white">
                     <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
                         <div v-if="!initialLoading" class="position-relative h-100">
@@ -178,7 +179,7 @@
                         </div>
                     </div>
                 </div>
-            </FlexPanel>
+            </FlexPanel-->
         </template>
     </div>
 </template>
@@ -217,6 +218,7 @@ import reportDefault from "./reportDefault";
 
 import WorkflowLint from "./Lint.vue";
 import MessagesModal from "./MessagesModal.vue";
+import NodeInspector from "./NodeInspector.vue";
 import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
 import SaveChangesModal from "./SaveChangesModal.vue";
 import StateUpgradeModal from "./StateUpgradeModal.vue";
@@ -225,12 +227,9 @@ import WorkflowGraph from "./WorkflowGraph.vue";
 import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
 import MarkdownToolBox from "@/components/Markdown/MarkdownToolBox.vue";
-import FlexPanel from "@/components/Panels/FlexPanel.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
 import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue";
 import UndoRedoStack from "@/components/UndoRedo/UndoRedoStack.vue";
-import FormDefault from "@/components/Workflow/Editor/Forms/FormDefault.vue";
-import FormTool from "@/components/Workflow/Editor/Forms/FormTool.vue";
 
 library.add(faArrowLeft, faArrowRight, faHistory);
 
@@ -238,12 +237,9 @@ export default {
     components: {
         ActivityBar,
         MarkdownEditor,
-        FlexPanel,
         SaveChangesModal,
         StateUpgradeModal,
         ToolPanel,
-        FormDefault,
-        FormTool,
         WorkflowAttributes,
         WorkflowLint,
         RefactorConfirmationModal,
@@ -253,6 +249,7 @@ export default {
         UndoRedoStack,
         WorkflowPanel,
         MarkdownToolBox,
+        NodeInspector,
     },
     props: {
         workflowId: {
diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
new file mode 100644
index 000000000000..4b506626242f
--- /dev/null
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -0,0 +1,41 @@
+<script setup lang="ts">
+import type { Step } from "@/stores/workflowStepStore";
+
+import Heading from "@/components/Common/Heading.vue";
+
+const props = defineProps<{
+    step: Step;
+}>();
+</script>
+
+<template>
+    <section class="tool-inspector">
+        <Heading h2 size="md">
+            {{ props.step.name }}
+        </Heading>
+    </section>
+</template>
+
+<style scoped lang="scss">
+@import "theme/blue.scss";
+
+.tool-inspector {
+    --clearance: 8px;
+    --width: 300px;
+
+    position: absolute;
+    right: 0;
+    top: var(--clearance);
+    bottom: var(--clearance);
+    width: var(--width);
+
+    background-color: $white;
+
+    z-index: 50000;
+    border-color: $border-color;
+    border-width: 1px;
+    border-style: solid;
+    border-radius: 0.5rem 0 0 0.5rem;
+    padding: 0.25rem 0.5rem;
+}
+</style>
diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue
index 14c83d2a8cbf..b0a27f1b6235 100644
--- a/client/src/components/Workflow/Editor/WorkflowGraph.vue
+++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue
@@ -64,6 +64,7 @@
             :viewport-bounding-box="viewportBoundingBox"
             @panBy="panBy"
             @moveTo="moveTo" />
+        <slot></slot>
     </div>
 </template>
 <script setup lang="ts">
diff --git a/client/src/composables/eventEmitter.ts b/client/src/composables/eventEmitter.ts
new file mode 100644
index 000000000000..13f95a4992ba
--- /dev/null
+++ b/client/src/composables/eventEmitter.ts
@@ -0,0 +1,20 @@
+import { type Ref, watch } from "vue";
+
+/**
+ * Emits an event whenever a value changes
+ * @param ref value to watch
+ * @param emit event emitter function
+ * @param name event to emit
+ */
+export function useEmit<Name extends string, T, F extends { (name: Name, arg: T): void }>(
+    ref: Ref<T>,
+    emit: F,
+    name: Name
+) {
+    watch(
+        () => ref.value,
+        () => {
+            emit(name, ref.value);
+        }
+    );
+}

From 71e8c7fc9e47a1525705fb718eaabfc662fb7da8 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 8 Oct 2024 16:29:12 +0200
Subject: [PATCH 029/131] modularize drag handle

---
 .../components/Common/DraggableSeparator.vue  | 32 +++++++++++++------
 client/src/components/Panels/FlexPanel.vue    |  9 ++++--
 2 files changed, 30 insertions(+), 11 deletions(-)

diff --git a/client/src/components/Common/DraggableSeparator.vue b/client/src/components/Common/DraggableSeparator.vue
index b722bb3c1802..716a02d78ab0 100644
--- a/client/src/components/Common/DraggableSeparator.vue
+++ b/client/src/components/Common/DraggableSeparator.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { useDebounce, useDraggable } from "@vueuse/core";
-import { computed, ref, watch } from "vue";
+import { useDebounce, useDraggable, useElementBounding } from "@vueuse/core";
+import { computed, onMounted, ref, watch } from "vue";
 
 import { useEmit } from "@/composables/eventEmitter";
 import { useClamp } from "@/composables/math";
@@ -14,14 +14,12 @@ const props = withDefaults(
         showDelay?: number;
         keyboardStepSize?: number;
         side: "left" | "right";
-        additionalOffset?: number;
     }>(),
     {
         showDelay: 100,
         keyboardStepSize: 50,
-        min: -Infinity,
+        min: 0,
         max: Infinity,
-        additionalOffset: 0,
     }
 );
 
@@ -34,6 +32,13 @@ const emit = defineEmits<{
 const { throttle } = useAnimationFrameThrottle();
 
 const draggable = ref<HTMLButtonElement | null>(null);
+const positionedParent = ref<HTMLElement | null>(null);
+
+onMounted(() => {
+    positionedParent.value = (draggable.value?.offsetParent as HTMLElement) ?? null;
+});
+
+const rootBoundingBox = useElementBounding(positionedParent);
 
 const { position: draggablePosition, isDragging } = useDraggable(draggable, {
     preventDefault: true,
@@ -56,8 +61,16 @@ watch(
     () => (handlePosition.value = props.position)
 );
 
+const borderWidth = 6;
+
 function updatePosition() {
-    handlePosition.value = draggablePosition.value.x;
+    if (props.side === "left") {
+        handlePosition.value = draggablePosition.value.x - rootBoundingBox.left.value + borderWidth;
+    } else {
+        const clientWidth = document.body.clientWidth;
+        const rootRightDistance = document.body.clientWidth - rootBoundingBox.right.value;
+        handlePosition.value = clientWidth - draggablePosition.value.x - rootRightDistance - borderWidth;
+    }
 }
 
 watch(
@@ -106,13 +119,12 @@ const style = computed(() => ({
 </template>
 
 <style scoped lang="scss">
-$border-width: 8px;
+$border-width: 6px;
 
 .drag-handle {
     --position: 0;
 
-    background-color: transparent;
-    background: none;
+    background-color: blue;
     border: none;
     border-radius: 0;
     position: absolute;
@@ -130,10 +142,12 @@ $border-width: 8px;
 
     &.side-left {
         left: var(--position);
+        margin-left: -$border-width;
     }
 
     &.side-right {
         right: var(--position);
+        margin-right: -$border-width;
     }
 }
 </style>
diff --git a/client/src/components/Panels/FlexPanel.vue b/client/src/components/Panels/FlexPanel.vue
index a01a8612b097..a9223a42e8a9 100644
--- a/client/src/components/Panels/FlexPanel.vue
+++ b/client/src/components/Panels/FlexPanel.vue
@@ -4,7 +4,7 @@ import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { computed, ref, watch } from "vue";
 
-import DraggableSeparator from "../Common/DraggableSeparator.vue";
+import DraggableSeparator from "@/components/Common/DraggableSeparator.vue";
 
 library.add(faChevronLeft, faChevronRight);
 
@@ -71,7 +71,12 @@ const sideClasses = computed(() => ({
         class="flex-panel"
         :class="{ ...sideClasses }"
         :style="`--width: ${panelWidth}px`">
-        <DraggableSeparator :position="panelWidth" :side="props.side"></DraggableSeparator>
+        <DraggableSeparator
+            :position="panelWidth"
+            :side="props.side"
+            :min="props.minWidth"
+            :max="props.maxWidth"
+            @positionChanged="(v) => (panelWidth = v)"></DraggableSeparator>
 
         <button
             v-if="props.collapsible"

From 9d77fc89e524e9497266a2297f2cea83dd8d89ae Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 8 Oct 2024 16:38:24 +0200
Subject: [PATCH 030/131] correct appearance of drag handle

---
 .../components/Common/DraggableSeparator.vue  | 29 +++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Common/DraggableSeparator.vue b/client/src/components/Common/DraggableSeparator.vue
index 716a02d78ab0..3778da1c413a 100644
--- a/client/src/components/Common/DraggableSeparator.vue
+++ b/client/src/components/Common/DraggableSeparator.vue
@@ -106,7 +106,7 @@ const style = computed(() => ({
     <button
         ref="draggable"
         class="drag-handle"
-        :class="`side-${props.side}`"
+        :class="[`side-${props.side}`, { show: showHover }]"
         :style="style"
         @mouseenter="hoverDraggable = true"
         @focusin="hoverDraggable = true"
@@ -119,12 +119,14 @@ const style = computed(() => ({
 </template>
 
 <style scoped lang="scss">
+@import "theme/blue.scss";
+
 $border-width: 6px;
 
 .drag-handle {
     --position: 0;
 
-    background-color: blue;
+    background-color: transparent;
     border: none;
     border-radius: 0;
     position: absolute;
@@ -149,5 +151,28 @@ $border-width: 6px;
         right: var(--position);
         margin-right: -$border-width;
     }
+
+    &::after {
+        display: block;
+        content: "";
+        position: absolute;
+        top: 0;
+        width: $border-width;
+        height: 100%;
+        background-color: transparent;
+        transition: background-color 0.1s;
+    }
+
+    &.show::after {
+        background-color: $brand-info;
+    }
+
+    &.side-left.show::after {
+        left: 0;
+    }
+
+    &.side-right.show::after {
+        right: 0;
+    }
 }
 </style>

From df3cbbfbaeb38268e712aecadd2d4c2731054b5c Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 8 Oct 2024 16:39:50 +0200
Subject: [PATCH 031/131] show collapse button

---
 client/src/components/Panels/FlexPanel.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Panels/FlexPanel.vue b/client/src/components/Panels/FlexPanel.vue
index a9223a42e8a9..299ee1e70e3c 100644
--- a/client/src/components/Panels/FlexPanel.vue
+++ b/client/src/components/Panels/FlexPanel.vue
@@ -76,7 +76,8 @@ const sideClasses = computed(() => ({
             :side="props.side"
             :min="props.minWidth"
             :max="props.maxWidth"
-            @positionChanged="(v) => (panelWidth = v)"></DraggableSeparator>
+            @positionChanged="(v) => (panelWidth = v)"
+            @visibilityChanged="(v) => (isHoveringDragHandle = v)"></DraggableSeparator>
 
         <button
             v-if="props.collapsible"

From 92096c78c8750e6f5aa1428ac6e04ce296081735 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 14 Oct 2024 18:53:49 +0200
Subject: [PATCH 032/131] add tool form to node inspector

---
 .../components/Common/DraggableSeparator.vue  | 15 ++++-
 client/src/components/Panels/FlexPanel.vue    |  8 ---
 .../src/components/Workflow/Editor/Index.vue  | 13 ++--
 .../Workflow/Editor/NodeInspector.vue         | 59 ++++++++++++++++---
 4 files changed, 75 insertions(+), 20 deletions(-)

diff --git a/client/src/components/Common/DraggableSeparator.vue b/client/src/components/Common/DraggableSeparator.vue
index 3778da1c413a..f63aa8f397a9 100644
--- a/client/src/components/Common/DraggableSeparator.vue
+++ b/client/src/components/Common/DraggableSeparator.vue
@@ -14,12 +14,14 @@ const props = withDefaults(
         showDelay?: number;
         keyboardStepSize?: number;
         side: "left" | "right";
+        inner?: boolean;
     }>(),
     {
         showDelay: 100,
         keyboardStepSize: 50,
         min: 0,
         max: Infinity,
+        inner: false,
     }
 );
 
@@ -106,7 +108,7 @@ const style = computed(() => ({
     <button
         ref="draggable"
         class="drag-handle"
-        :class="[`side-${props.side}`, { show: showHover }]"
+        :class="[`side-${props.side}`, { show: showHover, inner: props.inner }]"
         :style="style"
         @mouseenter="hoverDraggable = true"
         @focusin="hoverDraggable = true"
@@ -134,6 +136,7 @@ $border-width: 6px;
     padding: 0;
     height: 100%;
     z-index: 10000;
+    top: 0;
 
     --hover-expand: 4px;
 
@@ -144,11 +147,21 @@ $border-width: 6px;
 
     &.side-left {
         left: var(--position);
+
+        &.inner {
+            left: min(var(--position), calc(100% + 1px));
+        }
+
         margin-left: -$border-width;
     }
 
     &.side-right {
         right: var(--position);
+
+        &.inner {
+            right: min(var(--position), calc(100% + 1px));
+        }
+
         margin-right: -$border-width;
     }
 
diff --git a/client/src/components/Panels/FlexPanel.vue b/client/src/components/Panels/FlexPanel.vue
index 299ee1e70e3c..1dd518b66ed9 100644
--- a/client/src/components/Panels/FlexPanel.vue
+++ b/client/src/components/Panels/FlexPanel.vue
@@ -138,14 +138,6 @@ $border-width: 6px;
         background-color: $border-color;
     }
 
-    &.show-hover {
-        border-color: $brand-info;
-
-        &::after {
-            background-color: $brand-info;
-        }
-    }
-
     &.left {
         border-right-style: solid;
 
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 76164c234b3f..793acfa31c5c 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -149,7 +149,15 @@
                     @onChange="onChange"
                     @onRemove="onRemove"
                     @onUpdateStepPosition="onUpdateStepPosition">
-                    <NodeInspector v-if="activeStep" :step="activeStep"></NodeInspector>
+                    <NodeInspector
+                        v-if="activeStep"
+                        :step="activeStep"
+                        :datatypes="datatypes"
+                        @postJobActionsChanged="onChangePostJobActions"
+                        @annotationChanged="onAnnotation"
+                        @labelChanged="onLabel"
+                        @dataChanged="onSetData"
+                        @stepUpdated="updateStep"></NodeInspector>
                 </WorkflowGraph>
             </div>
             <!--FlexPanel side="right">
@@ -537,9 +545,6 @@ export default {
         hasActiveNodeDefault() {
             return this.activeStep && this.activeStep?.type != "tool";
         },
-        hasActiveNodeTool() {
-            return this.activeStep?.type == "tool";
-        },
     },
     watch: {
         id(newId, oldId) {
diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index 4b506626242f..e646f8f02972 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -1,18 +1,54 @@
 <script setup lang="ts">
+import { computed, ref } from "vue";
+
 import type { Step } from "@/stores/workflowStepStore";
 
-import Heading from "@/components/Common/Heading.vue";
+import FormTool from "./Forms/FormTool.vue";
+import DraggableSeparator from "@/components/Common/DraggableSeparator.vue";
 
 const props = defineProps<{
     step: Step;
+    datatypes: string[];
+}>();
+
+const emit = defineEmits<{
+    (e: "postJobActionsChanged", id: string, postJobActions: unknown): void;
+    (e: "annotationChanged", id: string, annotation: string): void;
+    (e: "labelChanged", id: string, label: string): void;
+    (e: "dataChanged", id: string, data: unknown): void;
+    (e: "stepUpdated", id: string, step: Step): void;
 }>();
+
+const width = ref(300);
+
+const cssVars = computed(() => ({
+    "--width": `${width.value}px`,
+}));
+
+const isTool = computed(() => props.step.type === "tool");
 </script>
 
 <template>
-    <section class="tool-inspector">
-        <Heading h2 size="md">
-            {{ props.step.name }}
-        </Heading>
+    <section class="tool-inspector" :style="cssVars">
+        <DraggableSeparator
+            inner
+            :position="width"
+            side="right"
+            :min="100"
+            :max="1200"
+            @positionChanged="(v) => (width = v)"></DraggableSeparator>
+
+        <div class="inspector-content">
+            <FormTool
+                v-if="isTool"
+                :step="props.step"
+                :datatypes="props.datatypes"
+                @onSetData="(id, d) => emit('dataChanged', id, d)"
+                @onUpdateStep="(id, s) => emit('stepUpdated', id, s)"
+                @onChangePostJobActions="(id, a) => emit('postJobActionsChanged', id, a)"
+                @onAnnotation="(id, a) => emit('annotationChanged', id, a)"
+                @onLabel="(id, l) => emit('labelChanged', id, l)"></FormTool>
+        </div>
     </section>
 </template>
 
@@ -27,7 +63,11 @@ const props = defineProps<{
     right: 0;
     top: var(--clearance);
     bottom: var(--clearance);
-    width: var(--width);
+    // add border width to node draggable separators width
+    width: calc(var(--width) + 1px);
+    max-width: calc(100% - var(--clearance));
+
+    overflow: hidden;
 
     background-color: $white;
 
@@ -36,6 +76,11 @@ const props = defineProps<{
     border-width: 1px;
     border-style: solid;
     border-radius: 0.5rem 0 0 0.5rem;
-    padding: 0.25rem 0.5rem;
+
+    .inspector-content {
+        overflow-y: auto;
+        height: 100%;
+        padding: 0.5rem 0.5rem;
+    }
 }
 </style>

From bc1248bb361a415f14bbfac28fdf339968e580f1 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 16 Oct 2024 15:07:54 +0200
Subject: [PATCH 033/131] add inspector heading

---
 .../Workflow/Editor/NodeInspector.vue         | 37 +++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index e646f8f02972..4bd7befee47b 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -1,10 +1,16 @@
 <script setup lang="ts">
+import { faCog, faTimes } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+import { BButton, BButtonGroup } from "bootstrap-vue";
+//@ts-ignore deprecated package without types (vue 2, remove this comment on vue 3 migration)
+import { ArrowLeftFromLine } from "lucide-vue";
 import { computed, ref } from "vue";
 
 import type { Step } from "@/stores/workflowStepStore";
 
 import FormTool from "./Forms/FormTool.vue";
 import DraggableSeparator from "@/components/Common/DraggableSeparator.vue";
+import Heading from "@/components/Common/Heading.vue";
 
 const props = defineProps<{
     step: Step;
@@ -26,6 +32,8 @@ const cssVars = computed(() => ({
 }));
 
 const isTool = computed(() => props.step.type === "tool");
+
+const title = computed(() => `${props.step.id + 1}: ${props.step.label ?? props.step.name}`);
 </script>
 
 <template>
@@ -38,6 +46,22 @@ const isTool = computed(() => props.step.type === "tool");
             :max="1200"
             @positionChanged="(v) => (width = v)"></DraggableSeparator>
 
+        <div class="inspector-heading">
+            <Heading h2 inline size="sm"> {{ title }} </Heading>
+
+            <BButtonGroup>
+                <BButton variant="link" size="md">
+                    <ArrowLeftFromLine absolute-stroke-width size="17" />
+                </BButton>
+                <BButton variant="link" size="md">
+                    <FontAwesomeIcon :icon="faCog" fixed-width />
+                </BButton>
+                <BButton variant="link" size="md">
+                    <FontAwesomeIcon :icon="faTimes" fixed-width />
+                </BButton>
+            </BButtonGroup>
+        </div>
+
         <div class="inspector-content">
             <FormTool
                 v-if="isTool"
@@ -77,10 +101,23 @@ const isTool = computed(() => props.step.type === "tool");
     border-style: solid;
     border-radius: 0.5rem 0 0 0.5rem;
 
+    .inspector-heading {
+        padding: 0.5rem;
+        display: flex;
+        justify-content: space-between;
+
+        button {
+            padding: 0.4rem;
+            display: grid;
+            place-items: center;
+        }
+    }
+
     .inspector-content {
         overflow-y: auto;
         height: 100%;
         padding: 0.5rem 0.5rem;
+        padding-top: 0;
     }
 }
 </style>

From b519f992dc9d45d8d63dd2557959ccc53045093e Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 17 Oct 2024 17:47:47 +0200
Subject: [PATCH 034/131] allow for saving default sizes for tools / steps

---
 client/src/components/Common/IdleLoad.vue     |  38 ++++++
 .../src/components/Workflow/Editor/Index.vue  |   3 +-
 .../Workflow/Editor/NodeInspector.vue         | 106 ++++++++++++----
 .../src/stores/workflowNodeInspectorStore.ts  | 114 ++++++++++++++++++
 4 files changed, 238 insertions(+), 23 deletions(-)
 create mode 100644 client/src/components/Common/IdleLoad.vue
 create mode 100644 client/src/stores/workflowNodeInspectorStore.ts

diff --git a/client/src/components/Common/IdleLoad.vue b/client/src/components/Common/IdleLoad.vue
new file mode 100644
index 000000000000..cf31fd7ee1a3
--- /dev/null
+++ b/client/src/components/Common/IdleLoad.vue
@@ -0,0 +1,38 @@
+<script setup lang="ts">
+// This component delays the rendering of slotted elements
+// in order to not slow down the rendering of parent components,
+// or the responsiveness of the UI
+
+import { BSpinner } from "bootstrap-vue";
+import { onMounted, ref } from "vue";
+
+const props = defineProps<{
+    spinner?: boolean;
+    center?: boolean;
+}>();
+
+const render = ref(false);
+const idleFallbackTime = 100;
+
+onMounted(() => {
+    if ("requestIdleCallback" in window) {
+        window.requestIdleCallback(() => (render.value = true));
+    } else {
+        setTimeout(() => (render.value = true), idleFallbackTime);
+    }
+});
+</script>
+
+<template>
+    <div class="idle-load" :class="{ center: props.center }">
+        <slot v-if="render"></slot>
+        <BSpinner v-else-if="props.spinner"></BSpinner>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.idle-load.center {
+    display: grid;
+    place-items: center;
+}
+</style>
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 793acfa31c5c..598f480d94e4 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -157,7 +157,8 @@
                         @annotationChanged="onAnnotation"
                         @labelChanged="onLabel"
                         @dataChanged="onSetData"
-                        @stepUpdated="updateStep"></NodeInspector>
+                        @stepUpdated="updateStep"
+                        @close="activeNodeId = null"></NodeInspector>
                 </WorkflowGraph>
             </div>
             <!--FlexPanel side="right">
diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index 4bd7befee47b..48f487da7806 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -1,16 +1,18 @@
 <script setup lang="ts">
 import { faCog, faTimes } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { BButton, BButtonGroup } from "bootstrap-vue";
+import { BButton, BButtonGroup, BDropdown, BDropdownForm, BDropdownItemButton, BFormCheckbox } from "bootstrap-vue";
 //@ts-ignore deprecated package without types (vue 2, remove this comment on vue 3 migration)
-import { ArrowLeftFromLine } from "lucide-vue";
-import { computed, ref } from "vue";
+import { ArrowLeftFromLine, ArrowRightToLine } from "lucide-vue";
+import { computed } from "vue";
 
+import { useWorkflowNodeInspectorStore } from "@/stores/workflowNodeInspectorStore";
 import type { Step } from "@/stores/workflowStepStore";
 
 import FormTool from "./Forms/FormTool.vue";
 import DraggableSeparator from "@/components/Common/DraggableSeparator.vue";
 import Heading from "@/components/Common/Heading.vue";
+import IdleLoad from "@/components/Common/IdleLoad.vue";
 
 const props = defineProps<{
     step: Step;
@@ -23,9 +25,13 @@ const emit = defineEmits<{
     (e: "labelChanged", id: string, label: string): void;
     (e: "dataChanged", id: string, data: unknown): void;
     (e: "stepUpdated", id: string, step: Step): void;
+    (e: "close"): void;
 }>();
 
-const width = ref(300);
+const inspectorStore = useWorkflowNodeInspectorStore();
+
+const maximized = computed(() => inspectorStore.maximized(props.step));
+const width = computed(() => inspectorStore.width(props.step));
 
 const cssVars = computed(() => ({
     "--width": `${width.value}px`,
@@ -34,44 +40,82 @@ const cssVars = computed(() => ({
 const isTool = computed(() => props.step.type === "tool");
 
 const title = computed(() => `${props.step.id + 1}: ${props.step.label ?? props.step.name}`);
+
+function close() {
+    inspectorStore.generalMaximized = false;
+    emit("close");
+}
 </script>
 
 <template>
-    <section class="tool-inspector" :style="cssVars">
+    <section class="tool-inspector" :class="{ maximized }" :style="cssVars">
         <DraggableSeparator
+            v-if="!maximized"
             inner
             :position="width"
             side="right"
             :min="100"
             :max="1200"
-            @positionChanged="(v) => (width = v)"></DraggableSeparator>
+            @positionChanged="(v) => inspectorStore.setWidth(props.step, v)"></DraggableSeparator>
 
         <div class="inspector-heading">
             <Heading h2 inline size="sm"> {{ title }} </Heading>
 
             <BButtonGroup>
-                <BButton variant="link" size="md">
-                    <ArrowLeftFromLine absolute-stroke-width size="17" />
+                <BButton
+                    v-if="!maximized"
+                    class="heading-button"
+                    variant="link"
+                    size="md"
+                    @click="inspectorStore.setMaximized(props.step, true)">
+                    <ArrowLeftFromLine absolute-stroke-width :size="17" />
                 </BButton>
-                <BButton variant="link" size="md">
-                    <FontAwesomeIcon :icon="faCog" fixed-width />
+                <BButton
+                    v-else
+                    class="heading-button"
+                    variant="link"
+                    size="md"
+                    @click="inspectorStore.setMaximized(props.step, false)">
+                    <ArrowRightToLine absolute-stroke-width :size="17" />
                 </BButton>
-                <BButton variant="link" size="md">
+
+                <BDropdown class="dropdown" toggle-class="heading-button" variant="link" size="md" no-caret>
+                    <template v-slot:button-content>
+                        <FontAwesomeIcon :icon="faCog" fixed-width />
+                    </template>
+
+                    <BDropdownForm form-class="px-2" title="remember size for all steps using this tool">
+                        <BFormCheckbox
+                            :checked="inspectorStore.isStored(props.step)"
+                            @input="(v) => inspectorStore.setStored(props.step, v)">
+                            remember size
+                        </BFormCheckbox>
+                    </BDropdownForm>
+
+                    <BDropdownItemButton @click="inspectorStore.clearAllStored">
+                        reset all stored sizes
+                    </BDropdownItemButton>
+                </BDropdown>
+
+                <BButton class="heading-button" variant="link" size="md" @click="close">
                     <FontAwesomeIcon :icon="faTimes" fixed-width />
                 </BButton>
             </BButtonGroup>
         </div>
 
         <div class="inspector-content">
-            <FormTool
-                v-if="isTool"
-                :step="props.step"
-                :datatypes="props.datatypes"
-                @onSetData="(id, d) => emit('dataChanged', id, d)"
-                @onUpdateStep="(id, s) => emit('stepUpdated', id, s)"
-                @onChangePostJobActions="(id, a) => emit('postJobActionsChanged', id, a)"
-                @onAnnotation="(id, a) => emit('annotationChanged', id, a)"
-                @onLabel="(id, l) => emit('labelChanged', id, l)"></FormTool>
+            <IdleLoad :key="props.step.id" spinner center class="w-100 h-50">
+                <FormTool
+                    v-if="isTool"
+                    class="w-100"
+                    :step="props.step"
+                    :datatypes="props.datatypes"
+                    @onSetData="(id, d) => emit('dataChanged', id, d)"
+                    @onUpdateStep="(id, s) => emit('stepUpdated', id, s)"
+                    @onChangePostJobActions="(id, a) => emit('postJobActionsChanged', id, a)"
+                    @onAnnotation="(id, a) => emit('annotationChanged', id, a)"
+                    @onLabel="(id, l) => emit('labelChanged', id, l)"></FormTool>
+            </IdleLoad>
         </div>
     </section>
 </template>
@@ -101,16 +145,34 @@ const title = computed(() => `${props.step.id + 1}: ${props.step.label ?? props.
     border-style: solid;
     border-radius: 0.5rem 0 0 0.5rem;
 
+    &.maximized {
+        border-radius: 0.5rem;
+        --clearance: 16px;
+        right: var(--clearance);
+        top: var(--clearance);
+        bottom: var(--clearance);
+        left: var(--clearance);
+        width: calc(100% - var(--clearance) * 2);
+    }
+
     .inspector-heading {
-        padding: 0.5rem;
+        padding: 0.25rem 0.5rem;
         display: flex;
         justify-content: space-between;
 
-        button {
+        h2 {
+            word-break: break-word;
+        }
+
+        &:deep(.heading-button) {
             padding: 0.4rem;
             display: grid;
             place-items: center;
         }
+
+        .dropdown.show {
+            display: inline-flex;
+        }
     }
 
     .inspector-content {
diff --git a/client/src/stores/workflowNodeInspectorStore.ts b/client/src/stores/workflowNodeInspectorStore.ts
new file mode 100644
index 000000000000..6591baf1c8bb
--- /dev/null
+++ b/client/src/stores/workflowNodeInspectorStore.ts
@@ -0,0 +1,114 @@
+import { useLocalStorage } from "@vueuse/core";
+import { defineStore } from "pinia";
+import { computed, del, ref, set } from "vue";
+
+import { ensureDefined } from "@/utils/assertions";
+import { getShortToolId } from "@/utils/tool";
+import { match } from "@/utils/utils";
+
+import type { Step } from "./workflowStepStore";
+
+interface StoredSize {
+    width: number;
+    maximized: boolean;
+}
+
+function getContentId(step: Step) {
+    return match(step.type, {
+        tool: () => `tool_${getShortToolId(ensureDefined(step.content_id))}`,
+        subworkflow: () => `subworkflow_${step.content_id}`,
+        data_collection_input: () => "input",
+        data_input: () => "input",
+        parameter_input: () => "input",
+        pause: () => "pause",
+    });
+}
+
+export const useWorkflowNodeInspectorStore = defineStore("workflowNodeInspectorStore", () => {
+    /** width of the node inspector if no other width is stored */
+    const generalWidth = useLocalStorage("workflowNodeInspectorGeneralWidth", 300);
+    /** maximized state of the node inspector if no other value is stored */
+    const generalMaximized = ref(false);
+    const storedSizes = useLocalStorage<Record<string, StoredSize>>("workflowNodeInspectorStoredSizes", {});
+
+    const isStored = computed(() => (step: Step) => {
+        const id = getContentId(step);
+        const storedIds = new Set(Object.keys(storedSizes.value));
+
+        return storedIds.has(id);
+    });
+
+    function setStored(step: Step, stored: boolean) {
+        const id = getContentId(step);
+        const storedValue = storedSizes.value[id];
+
+        if (stored) {
+            if (!storedValue) {
+                set(storedSizes.value, id, {
+                    width: generalWidth.value,
+                    maximized: generalMaximized.value,
+                });
+            }
+        } else {
+            if (storedValue) {
+                generalWidth.value = storedValue.width;
+                generalMaximized.value = storedValue.maximized;
+                del(storedSizes.value, id);
+            }
+        }
+    }
+
+    const width = computed(() => (step: Step) => {
+        const id = getContentId(step);
+        return storedSizes.value[id]?.width ?? generalWidth.value;
+    });
+
+    /**
+     * sets the inspectors width. If the width is stored,
+     * stores the new width, otherwise, updates the general width
+     */
+    function setWidth(step: Step, newWidth: number) {
+        const id = getContentId(step);
+
+        if (storedSizes.value[id]) {
+            storedSizes.value[id]!.width = newWidth;
+        } else {
+            generalWidth.value = newWidth;
+        }
+    }
+
+    const maximized = computed(() => (step: Step) => {
+        const id = getContentId(step);
+        return storedSizes.value[id]?.maximized ?? generalMaximized.value;
+    });
+
+    /**
+     * sets the inspectors maximized state. If the state is stored,
+     * stores the maximized state, otherwise, updates the general maximized state
+     */
+    function setMaximized(step: Step, newMaximized: boolean) {
+        const id = getContentId(step);
+
+        if (storedSizes.value[id]) {
+            storedSizes.value[id]!.maximized = newMaximized;
+        } else {
+            generalMaximized.value = newMaximized;
+        }
+    }
+
+    function clearAllStored() {
+        storedSizes.value = {};
+    }
+
+    return {
+        isStored,
+        setStored,
+        clearAllStored,
+        generalWidth,
+        generalMaximized,
+        width,
+        setWidth,
+        maximized,
+        setMaximized,
+    };
+});

From 3dbe6f81c9c246897a7a2fb3c765aa6474065759 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:21:15 +0200
Subject: [PATCH 035/131] make double click maximize node inspector

---
 .../src/components/Workflow/Editor/Node.vue   | 62 ++++++++++++++++++-
 .../Workflow/Editor/composables/d3Zoom.ts     | 19 +++++-
 2 files changed, 75 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue
index 4af194ad89a6..ea9aa2ae19e3 100644
--- a/client/src/components/Workflow/Editor/Node.vue
+++ b/client/src/components/Workflow/Editor/Node.vue
@@ -18,7 +18,8 @@
         <div
             class="unselectable clearfix card-header py-1 px-2"
             :class="headerClass"
-            @click.exact="makeActive"
+            @pointerdown.exact="onPointerDown"
+            @pointerup.exact="onPointerUp"
             @click.shift.capture.prevent.stop="toggleSelected"
             @keyup.enter="makeActive">
             <b-button-group class="float-right">
@@ -91,7 +92,8 @@
             variant="danger"
             show
             class="node-error m-0 rounded-0 rounded-bottom"
-            @click.exact="makeActive"
+            @pointerdown.exact="onPointerDown"
+            @pointerup.exact="onPointerUp"
             @click.shift.capture.prevent.stop="toggleSelected">
             {{ errors }}
         </b-alert>
@@ -100,7 +102,8 @@
             v-else
             class="node-body position-relative card-body p-0 mx-2"
             :class="{ 'cursor-pointer': isInvocation }"
-            @click.exact="makeActive"
+            @pointerdown.exact="onPointerDown"
+            @pointerup.exact="onPointerUp"
             @click.shift.capture.prevent.stop="toggleSelected"
             @keyup.enter="makeActive">
             <NodeInput
@@ -160,6 +163,7 @@ import WorkflowIcons from "@/components/Workflow/icons";
 import type { GraphStep } from "@/composables/useInvocationGraph";
 import { useWorkflowStores } from "@/composables/workflowStores";
 import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore";
+import { useWorkflowNodeInspectorStore } from "@/stores/workflowNodeInspectorStore";
 import type { Step } from "@/stores/workflowStepStore";
 
 import { ToggleStepSelectedAction } from "./Actions/stepActions";
@@ -224,12 +228,14 @@ const postJobActions = computed(() => props.step.post_job_actions || {});
 const workflowOutputs = computed(() => props.step.workflow_outputs || []);
 const { connectionStore, stateStore, stepStore, undoRedoStore } = useWorkflowStores();
 const isLoading = computed(() => Boolean(stateStore.getStepLoadingState(props.id)?.loading));
+
 useNodePosition(
     elHtml,
     props.id,
     stateStore,
     computed(() => props.scale)
 );
+
 const title = computed(() => props.step.label || props.step.name);
 const idString = computed(() => `wf-node-step-${props.id}`);
 const showRule = computed(() => props.step.inputs?.length > 0 && props.step.outputs?.length > 0);
@@ -247,10 +253,13 @@ const classes = computed(() => {
         "node-multi-selected": stateStore.getStepMultiSelected(props.id),
     };
 });
+
 const style = computed(() => {
     return { top: props.step.position!.top + "px", left: props.step.position!.left + "px" };
 });
+
 const errors = computed(() => props.step.errors || stateStore.getStepLoadingState(props.id)?.error);
+
 const headerClass = computed(() => {
     return {
         ...invocationStep.value.headerClass,
@@ -259,6 +268,7 @@ const headerClass = computed(() => {
         "cursor-move": !props.readonly && !props.isInvocation,
     };
 });
+
 const inputs = computed(() => {
     const connections = connectionStore.getConnectionsForStep(props.id);
     const extraStepInputs = stepStore.getStepExtraInputs(props.id);
@@ -275,6 +285,7 @@ const inputs = computed(() => {
     });
     return [...stepInputs, ...invalidInputTerminalSource];
 });
+
 const invalidOutputs = computed(() => {
     const connections = connectionStore.getConnectionsForStep(props.id);
     const invalidConnections = connections.filter(
@@ -287,6 +298,7 @@ const invalidOutputs = computed(() => {
         return { name, optional: false, datatypes: [], valid: false };
     });
 });
+
 const invocationStep = computed(() => props.step as GraphStep);
 const outputs = computed(() => {
     return [...props.step.outputs, ...invalidOutputs.value];
@@ -296,7 +308,49 @@ function onDragConnector(dragPosition: TerminalPosition, terminal: OutputTermina
     emit("onDragConnector", dragPosition, terminal);
 }
 
+const mouseMovementThreshold = 9;
+const singleClickTimeout = 800;
+const doubleClickTimeout = 500;
+
+let mouseDownTime = 0;
+let doubleClickTime = 0;
+
+let movementDistance = 0;
+let lastPosition: XYPosition | null = null;
+
+const inspectorStore = useWorkflowNodeInspectorStore();
+
+function onPointerDown() {
+    mouseDownTime = Date.now();
+}
+
+function onPointerUp() {
+    const mouseUpTime = Date.now();
+    const clickTime = mouseUpTime - mouseDownTime;
+
+    if (clickTime <= singleClickTimeout && movementDistance <= mouseMovementThreshold) {
+        makeActive();
+    }
+
+    const timeBetweenClicks = mouseUpTime - doubleClickTime;
+
+    if (timeBetweenClicks < doubleClickTimeout) {
+        inspectorStore.setMaximized(props.step, true);
+    }
+
+    doubleClickTime = Date.now();
+    lastPosition = null;
+    movementDistance = 0;
+}
+
 function onMoveTo(position: XYPosition) {
+    if (lastPosition) {
+        movementDistance += Math.abs(position.x - lastPosition.x);
+        movementDistance += Math.abs(position.y - lastPosition.y);
+    }
+
+    lastPosition = position;
+
     emit("onUpdateStepPosition", props.id, {
         top: position.y + props.scroll.y.value / props.scale,
         left: position.x + props.scroll.x.value / props.scale,
@@ -337,6 +391,8 @@ function toggleSelected() {
 @import "theme/blue.scss";
 
 .workflow-node {
+    --dblclick: prevent;
+
     position: absolute;
     z-index: 100;
     width: $workflow-node-width;
diff --git a/client/src/components/Workflow/Editor/composables/d3Zoom.ts b/client/src/components/Workflow/Editor/composables/d3Zoom.ts
index f8b36e5ddc09..d0f708514334 100644
--- a/client/src/components/Workflow/Editor/composables/d3Zoom.ts
+++ b/client/src/components/Workflow/Editor/composables/d3Zoom.ts
@@ -7,9 +7,22 @@ import { type XYPosition } from "@/stores/workflowEditorStateStore";
 
 // if element is draggable it may implement its own drag handler,
 // but d3zoom would call preventDefault
-const filter = (event: any) => {
-    const preventZoom = event.target.classList.contains("prevent-zoom");
-    return !preventZoom;
+const filter = (event: D3ZoomEvent<HTMLElement, unknown>["sourceEvent"]) => {
+    const target = event.target as HTMLElement;
+
+    if (target.classList.contains("prevent-zoom")) {
+        return false;
+    }
+
+    if (event.type === "dblclick") {
+        const style = getComputedStyle(target);
+
+        if (style.getPropertyValue("--dblclick") === "prevent") {
+            return false;
+        }
+    }
+
+    return true;
 };
 
 export function useD3Zoom(

From 3f83f53276ab8b05547a236bcc8477e64701390f Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:33:05 +0200
Subject: [PATCH 036/131] fix scroll height

---
 client/src/components/Workflow/Editor/NodeInspector.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index 48f487da7806..21db2b2b8542 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -144,6 +144,8 @@ function close() {
     border-width: 1px;
     border-style: solid;
     border-radius: 0.5rem 0 0 0.5rem;
+    display: flex;
+    flex-direction: column;
 
     &.maximized {
         border-radius: 0.5rem;
@@ -177,7 +179,6 @@ function close() {
 
     .inspector-content {
         overflow-y: auto;
-        height: 100%;
         padding: 0.5rem 0.5rem;
         padding-top: 0;
     }

From 4f8727a38d38fd33cf44c97e159956bbeab4ddfb Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:50:46 +0200
Subject: [PATCH 037/131] add form default to inspector

---
 .../src/components/Workflow/Editor/Index.vue  | 30 ++-----------------
 .../Workflow/Editor/NodeInspector.vue         | 14 +++++++++
 2 files changed, 16 insertions(+), 28 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 598f480d94e4..17d10ce6c09c 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -158,37 +158,11 @@
                         @labelChanged="onLabel"
                         @dataChanged="onSetData"
                         @stepUpdated="updateStep"
+                        @editSubworkflow="onEditSubworkflow"
+                        @attemptRefactor="onAttemptRefactor"
                         @close="activeNodeId = null"></NodeInspector>
                 </WorkflowGraph>
             </div>
-            <!--FlexPanel side="right">
-                <div class="unified-panel bg-white">
-                    <div ref="rightPanelElement" class="unified-panel-body workflow-right p-2">
-                        <div v-if="!initialLoading" class="position-relative h-100">
-                            <FormTool
-                                v-if="hasActiveNodeTool"
-                                :key="activeStep.id"
-                                :step="activeStep"
-                                :datatypes="datatypes"
-                                @onChangePostJobActions="onChangePostJobActions"
-                                @onAnnotation="onAnnotation"
-                                @onLabel="onLabel"
-                                @onSetData="onSetData"
-                                @onUpdateStep="updateStep" />
-                            <FormDefault
-                                v-else-if="hasActiveNodeDefault"
-                                :step="activeStep"
-                                :datatypes="datatypes"
-                                @onAnnotation="onAnnotation"
-                                @onLabel="onLabel"
-                                @onEditSubworkflow="onEditSubworkflow"
-                                @onAttemptRefactor="onAttemptRefactor"
-                                @onSetData="onSetData"
-                                @onUpdateStep="updateStep" />
-                        </div>
-                    </div>
-                </div>
-            </FlexPanel-->
         </template>
     </div>
 </template>
diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index 21db2b2b8542..d933919ef3ef 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -9,6 +9,7 @@ import { computed } from "vue";
 import { useWorkflowNodeInspectorStore } from "@/stores/workflowNodeInspectorStore";
 import type { Step } from "@/stores/workflowStepStore";
 
+import FormDefault from "./Forms/FormDefault.vue";
 import FormTool from "./Forms/FormTool.vue";
 import DraggableSeparator from "@/components/Common/DraggableSeparator.vue";
 import Heading from "@/components/Common/Heading.vue";
@@ -25,6 +26,8 @@ const emit = defineEmits<{
     (e: "labelChanged", id: string, label: string): void;
     (e: "dataChanged", id: string, data: unknown): void;
     (e: "stepUpdated", id: string, step: Step): void;
+    (e: "editSubworkflow", id: string): void;
+    (e: "attemptRefactor", ...args: any[]): void;
     (e: "close"): void;
 }>();
 
@@ -115,6 +118,16 @@ function close() {
                     @onChangePostJobActions="(id, a) => emit('postJobActionsChanged', id, a)"
                     @onAnnotation="(id, a) => emit('annotationChanged', id, a)"
                     @onLabel="(id, l) => emit('labelChanged', id, l)"></FormTool>
+                <FormDefault
+                    v-else
+                    :step="props.step"
+                    :datatypes="datatypes"
+                    @onSetData="(id, d) => emit('dataChanged', id, d)"
+                    @onUpdateStep="(id, s) => emit('stepUpdated', id, s)"
+                    @onAnnotation="(id, a) => emit('annotationChanged', id, a)"
+                    @onLabel="(id, l) => emit('labelChanged', id, l)"
+                    @onEditSubworkflow="(id) => emit('editSubworkflow', id)"
+                    @onAttemptRefactor="(...args) => emit('attemptRefactor', ...args)" />
             </IdleLoad>
         </div>
     </section>
@@ -179,6 +192,7 @@ function close() {
 
     .inspector-content {
         overflow-y: auto;
+        overflow-x: hidden;
         padding: 0.5rem 0.5rem;
         padding-top: 0;
     }

From 8efcc273d862d8bbdc27e72353e6067db152dc82 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 21 Oct 2024 12:43:33 +0200
Subject: [PATCH 038/131] make form default full width

---
 client/src/components/Common/IdleLoad.vue               | 2 +-
 client/src/components/Workflow/Editor/NodeInspector.vue | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Common/IdleLoad.vue b/client/src/components/Common/IdleLoad.vue
index cf31fd7ee1a3..cb0aeed58026 100644
--- a/client/src/components/Common/IdleLoad.vue
+++ b/client/src/components/Common/IdleLoad.vue
@@ -12,7 +12,7 @@ const props = defineProps<{
 }>();
 
 const render = ref(false);
-const idleFallbackTime = 100;
+const idleFallbackTime = 250;
 
 onMounted(() => {
     if ("requestIdleCallback" in window) {
diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index d933919ef3ef..2013b720985f 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -120,6 +120,7 @@ function close() {
                     @onLabel="(id, l) => emit('labelChanged', id, l)"></FormTool>
                 <FormDefault
                     v-else
+                    class="w-100"
                     :step="props.step"
                     :datatypes="datatypes"
                     @onSetData="(id, d) => emit('dataChanged', id, d)"

From e2c1558d03020ff4c97b671bf9676d58aafff307 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 21 Oct 2024 12:54:56 +0200
Subject: [PATCH 039/131] remove ability to sub-workflow own workflow

---
 client/src/components/Panels/WorkflowPanel.vue           | 5 +++++
 client/src/components/Workflow/Editor/Index.vue          | 1 +
 .../components/Workflow/List/WorkflowActionsExtend.vue   | 9 +++++++--
 client/src/components/Workflow/List/WorkflowCard.vue     | 3 +++
 client/src/components/Workflow/List/WorkflowCardList.vue | 2 ++
 5 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index 4028c896682d..bbaec6cc17f0 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -11,6 +11,10 @@ import DelayedInput from "@/components/Common/DelayedInput.vue";
 import ScrollToTopButton from "@/components/ToolsList/ScrollToTopButton.vue";
 import WorkflowCardList from "@/components/Workflow/List/WorkflowCardList.vue";
 
+const props = defineProps<{
+    currentWorkflowId: string;
+}>();
+
 const emit = defineEmits<{
     (e: "insertWorkflow", id: string, name: string): void;
     (e: "insertWorkflowSteps", id: string, stepCount: number): void;
@@ -124,6 +128,7 @@ function scrollToTop() {
                 :hide-runs="true"
                 :workflows="workflows"
                 :filterable="false"
+                :current-workflow-id="props.currentWorkflowId"
                 editor-view
                 @insertWorkflow="(...args) => emit('insertWorkflow', ...args)"
                 @insertWorkflowSteps="(...args) => emit('insertWorkflowSteps', ...args)" />
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 17d10ce6c09c..dcbda762bcc5 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -66,6 +66,7 @@
                 <UndoRedoStack v-else-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
                 <WorkflowPanel
                     v-else-if="isActiveSideBar('workflow-editor-workflows')"
+                    :current-workflow-id="id"
                     @insertWorkflow="onInsertWorkflow"
                     @insertWorkflowSteps="onInsertWorkflowSteps" />
                 <WorkflowAttributes
diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
index abe6ed959ea7..6da91c1a8a66 100644
--- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue
+++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
@@ -28,11 +28,13 @@ interface Props {
     workflow: any;
     published?: boolean;
     editor?: boolean;
+    current?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
     published: false,
     editor: false,
+    current: false,
 });
 
 const emit = defineEmits<{
@@ -161,8 +163,10 @@ const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflo
         </BButtonGroup>
 
         <div>
+            <i v-if="props.current" class="mr-2"> current workflow </i>
+
             <BButton
-                v-if="!isAnonymous && !shared"
+                v-if="!isAnonymous && !shared && !props.current"
                 v-b-tooltip.hover.noninteractive
                 :disabled="workflow.deleted"
                 size="sm"
@@ -175,7 +179,7 @@ const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflo
             </BButton>
 
             <AsyncButton
-                v-else
+                v-else-if="!props.current"
                 v-b-tooltip.hover.noninteractive
                 size="sm"
                 :disabled="isAnonymous"
@@ -203,6 +207,7 @@ const { copyPublicLink, copyWorkflow, downloadUrl, importWorkflow } = useWorkflo
                 </BButton>
 
                 <BButton
+                    v-if="!props.current"
                     v-b-tooltip.hover.noninteractive
                     size="sm"
                     title="Insert as sub-workflow"
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 4a8370e2bfe6..6650a0385fdc 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -22,6 +22,7 @@ interface Props {
     filterable?: boolean;
     publishedView?: boolean;
     editorView?: boolean;
+    current?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -30,6 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
     hideRuns: false,
     filterable: true,
     editorView: false,
+    current: false,
 });
 
 const emit = defineEmits<{
@@ -145,6 +147,7 @@ const dropdownOpen = ref(false);
                     :workflow="workflow"
                     :published="publishedView"
                     :editor="editorView"
+                    :current="props.current"
                     @refreshList="emit('refreshList', true)"
                     @insert="(...args) => emit('insert', ...args)"
                     @insertSteps="(...args) => emit('insertSteps', ...args)" />
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index 91a3b9f91151..dd368b76cb7a 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -15,6 +15,7 @@ interface Props {
     filterable?: boolean;
     publishedView?: boolean;
     editorView?: boolean;
+    currentWorkflowId?: string;
 }
 
 const props = defineProps<Props>();
@@ -78,6 +79,7 @@ function onInsertSteps(workflow: Workflow) {
             :filterable="props.filterable"
             :published-view="props.publishedView"
             :editor-view="props.editorView"
+            :current="workflow.id === props.currentWorkflowId"
             class="workflow-card"
             @tagClick="(...args) => emit('tagClick', ...args)"
             @refreshList="(...args) => emit('refreshList', ...args)"

From a3c702cb8d557b03d55413917691f5aef8ca1ee7 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 21 Oct 2024 13:13:58 +0200
Subject: [PATCH 040/131] connect refresh list signal

---
 client/src/components/Panels/WorkflowPanel.vue | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index bbaec6cc17f0..670a12710e1f 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -87,10 +87,23 @@ async function load() {
     }
 }
 
+function resetWorkflows() {
+    workflows.value = [];
+    totalWorkflowsCount.value = Infinity;
+    loading.value = false;
+    fetchKey = "";
+}
+
+function refresh() {
+    resetWorkflows();
+    getWorkflows.clear();
+    load();
+}
+
 watchImmediate(
     () => filterText.value,
     () => {
-        workflows.value = [];
+        resetWorkflows();
         fetchKey = filterText.value;
         load();
     }
@@ -131,7 +144,8 @@ function scrollToTop() {
                 :current-workflow-id="props.currentWorkflowId"
                 editor-view
                 @insertWorkflow="(...args) => emit('insertWorkflow', ...args)"
-                @insertWorkflowSteps="(...args) => emit('insertWorkflowSteps', ...args)" />
+                @insertWorkflowSteps="(...args) => emit('insertWorkflowSteps', ...args)"
+                @refreshList="refresh" />
 
             <div v-if="allLoaded || filterText !== ''" class="list-end">
                 <span v-if="workflows.length == 1"> - 1 workflow loaded - </span>

From bd723eff633b9d28475f48e5b43c2d8439b5ef4b Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 21 Oct 2024 13:21:15 +0200
Subject: [PATCH 041/131] fix workflow creation

---
 client/src/components/Workflow/Editor/Index.vue         | 6 +++++-
 client/src/components/Workflow/List/WorkflowActions.vue | 4 +++-
 client/src/components/Workflow/List/WorkflowCard.vue    | 1 +
 3 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index dcbda762bcc5..7efe6c120ca2 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -732,7 +732,11 @@ export default {
             }
 
             if (activityId === "save-workflow") {
-                this.onSave();
+                if (this.isNewTempWorkflow) {
+                    await this.onCreate();
+                } else {
+                    await this.onSave();
+                }
             }
         },
         onLayout() {
diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue
index bbedf649ab0a..3bcdc2427fb7 100644
--- a/client/src/components/Workflow/List/WorkflowActions.vue
+++ b/client/src/components/Workflow/List/WorkflowActions.vue
@@ -26,11 +26,13 @@ interface Props {
     workflow: any;
     published?: boolean;
     editor?: boolean;
+    current?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
     published: false,
     editor: false,
+    current: false,
 });
 
 const emit = defineEmits<{
@@ -151,7 +153,7 @@ const runPath = computed(
                 </template>
 
                 <BDropdownItem
-                    v-if="!isAnonymous && !shared && !props.workflow.deleted"
+                    v-if="!isAnonymous && !shared && !props.workflow.deleted && !props.current"
                     class="workflow-delete-button"
                     title="Delete workflow"
                     size="sm"
diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index 6650a0385fdc..b7cf49328f21 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -100,6 +100,7 @@ const dropdownOpen = ref(false);
                         :workflow="props.workflow"
                         :published="props.publishedView"
                         :editor="props.editorView"
+                        :current="props.current"
                         @refreshList="emit('refreshList', true)"
                         @dropdown="(open) => (dropdownOpen = open)" />
                 </div>

From dcc65a95fe2509900a1e33cf396c169276f8ad07 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 22 Oct 2024 15:36:24 +0200
Subject: [PATCH 042/131] make current workflow immutable from card

---
 client/src/components/Workflow/List/WorkflowCard.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index b7cf49328f21..cc819e5dd63d 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -114,7 +114,7 @@ const dropdownOpen = ref(false);
                         {{ workflow.name }}
                     </BLink>
                     <BButton
-                        v-if="!shared && !workflow.deleted"
+                        v-if="!props.current && !shared && !workflow.deleted"
                         v-b-tooltip.hover.noninteractive
                         :data-workflow-rename="workflow.id"
                         class="inline-icon-button workflow-rename"
@@ -138,7 +138,7 @@ const dropdownOpen = ref(false);
                     <StatelessTags
                         clickable
                         :value="workflow.tags"
-                        :disabled="isAnonymous || workflow.deleted || shared"
+                        :disabled="props.current || isAnonymous || workflow.deleted || shared"
                         :max-visible-tags="gridView ? 2 : 8"
                         @input="onTagsUpdate($event)"
                         @tag-click="onTagClick($event)" />

From 1ce4376fe2725ed0e6c75d6895d29a6c85e3bc78 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 22 Oct 2024 15:47:03 +0200
Subject: [PATCH 043/131] replace save with save and exit

---
 .../Workflow/Editor/modules/activities.ts     | 28 +++++--------------
 1 file changed, 7 insertions(+), 21 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index e99e1c68568a..10729d6aaa9e 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -126,16 +126,6 @@ export const workflowEditorActivities = [
         click: true,
         optional: true,
     },
-    {
-        description: "Save this workflow, then exit the workflow editor.",
-        icon: faSave,
-        id: "save-and-exit",
-        title: "Save + Exit",
-        tooltip: "Save and Exit",
-        visible: false,
-        click: true,
-        optional: true,
-    },
     {
         description: "Exit the workflow editor and return to the start screen.",
         icon: faSignOutAlt,
@@ -156,27 +146,23 @@ interface SpecialActivityOptions {
 
 export function useSpecialWorkflowActivities(options: Ref<SpecialActivityOptions>) {
     const saveHover = computed(() => {
-        if (options.value.isNewTempWorkflow) {
-            return "Save Workflow";
-        } else if (!options.value.hasChanges) {
-            return "Workflow has no changes";
-        } else if (options.value.hasInvalidConnections) {
+        if (options.value.hasInvalidConnections) {
             return "Workflow has invalid connections, review and remove invalid connections";
         } else {
-            return "Save Workflow";
+            return "Save this workflow, then exit the workflow editor";
         }
     });
 
     const specialWorkflowActivities = computed<Activity[]>(() => [
         {
-            title: "Save",
-            tooltip: saveHover.value,
-            description: "Save changes made to this workflow.",
+            description: "",
             icon: faSave,
-            id: "save-workflow",
+            id: "save-and-exit",
+            title: "Save + Exit",
+            tooltip: saveHover.value,
+            visible: false,
             click: true,
             mutable: false,
-            variant: options.value.hasChanges ? "primary" : "disabled",
         },
     ]);
 

From 410be186ef53222fa17f38e3164bebb92f370260 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 22 Oct 2024 15:54:24 +0200
Subject: [PATCH 044/131] add save button to title bar

---
 .../src/components/Workflow/Editor/Index.vue  | 38 +++++++++++++------
 1 file changed, 26 insertions(+), 12 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 7efe6c120ca2..b2ff116e71f7 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -118,6 +118,14 @@
                         <span class="editor-title" :title="name"
                             >{{ name }}
                             <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
+                            <b-button
+                                v-if="hasChanges"
+                                class="py-1 px-2"
+                                variant="link"
+                                :title="saveWorkflowTitle"
+                                @click="saveOrCreate">
+                                <FontAwesomeIcon :icon="faSave" />
+                            </b-button>
                         </span>
                     </span>
 
@@ -170,7 +178,7 @@
 
 <script>
 import { library } from "@fortawesome/fontawesome-svg-core";
-import { faArrowLeft, faArrowRight, faCog, faHistory, faTimes } from "@fortawesome/free-solid-svg-icons";
+import { faArrowLeft, faArrowRight, faCog, faHistory, faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { whenever } from "@vueuse/core";
 import { logicAnd, logicNot, logicOr } from "@vueuse/math";
@@ -444,6 +452,12 @@ export default {
             }))
         );
 
+        const saveWorkflowTitle = computed(() =>
+            hasInvalidConnections.value
+                ? "Workflow has invalid connections, review and remove invalid connections"
+                : "Save Workflow"
+        );
+
         return {
             id,
             name,
@@ -485,6 +499,7 @@ export default {
             insertMarkdown,
             specialWorkflowActivities,
             isNewTempWorkflow,
+            saveWorkflowTitle,
         };
     },
     data() {
@@ -512,6 +527,7 @@ export default {
             workflowEditorActivities,
             faTimes,
             faCog,
+            faSave,
         };
     },
     computed: {
@@ -700,14 +716,16 @@ export default {
         onSaveAs() {
             this.showSaveAsModal = true;
         },
+        async saveOrCreate() {
+            if (this.isNewTempWorkflow) {
+                await this.onCreate();
+            } else {
+                await this.onSave();
+            }
+        },
         async onActivityClicked(activityId) {
             if (activityId === "save-and-exit") {
-                if (this.isNewTempWorkflow) {
-                    await this.onCreate();
-                } else {
-                    await this.onSave();
-                }
-
+                await this.saveOrCreate();
                 this.$router.push("/workflows/list");
             }
 
@@ -732,11 +750,7 @@ export default {
             }
 
             if (activityId === "save-workflow") {
-                if (this.isNewTempWorkflow) {
-                    await this.onCreate();
-                } else {
-                    await this.onSave();
-                }
+                await this.saveOrCreate();
             }
         },
         onLayout() {

From 205e6d39518fa85bb889a660dc136c4dbc8f2eac Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 24 Oct 2024 14:24:16 +0200
Subject: [PATCH 045/131] move initial position to not interfere with toolbar

---
 client/src/components/Workflow/Editor/Index.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index b2ff116e71f7..c087a21f99c9 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -150,6 +150,7 @@
                     :datatypes-mapper="datatypesMapper"
                     :highlight-id="highlightId"
                     :scroll-to-id="scrollToId"
+                    :initial-position="{ x: 50, y: 50 }"
                     @scrollTo="scrollToId = null"
                     @transform="(value) => (transform = value)"
                     @graph-offset="(value) => (graphOffset = value)"

From 60a0df43a4fbb1d903a2fbed78331d63196eee07 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 24 Oct 2024 17:34:02 +0200
Subject: [PATCH 046/131] make step annotation resizable

---
 client/src/style/scss/ui.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/style/scss/ui.scss b/client/src/style/scss/ui.scss
index d29d05759050..7a540d9fef5f 100644
--- a/client/src/style/scss/ui.scss
+++ b/client/src/style/scss/ui.scss
@@ -224,7 +224,7 @@ $ui-margin-horizontal-large: $margin-v * 2;
 
 .ui-textarea {
     @extend .ui-input;
-    height: 100px !important;
+    min-height: 100px;
 }
 
 .ui-switch {

From f02441e344a9f33b96788025a1eabd2cde9f9f44 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:11:23 +0100
Subject: [PATCH 047/131] fix activity item test

---
 client/src/components/ActivityBar/ActivityItem.test.js | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityItem.test.js b/client/src/components/ActivityBar/ActivityItem.test.js
index d4486230dda9..df919558fbae 100644
--- a/client/src/components/ActivityBar/ActivityItem.test.js
+++ b/client/src/components/ActivityBar/ActivityItem.test.js
@@ -28,8 +28,7 @@ describe("ActivityItem", () => {
     });
 
     it("rendering", async () => {
-        const reference = wrapper.find("[id='activity-test-id']");
-        expect(reference.attributes().id).toBe("activity-test-id");
+        const reference = wrapper.find(".activity-item");
         expect(reference.text()).toBe("activity-test-title");
         expect(reference.find("[icon='activity-test-icon']").exists()).toBeTruthy();
         expect(reference.find(".progress").exists()).toBeFalsy();
@@ -45,7 +44,7 @@ describe("ActivityItem", () => {
     });
 
     it("rendering indicator", async () => {
-        const reference = wrapper.find("[id='activity-test-id']");
+        const reference = wrapper.find(".activity-item");
         const indicatorSelector = "[data-description='activity indicator']";
         const noindicator = reference.find(indicatorSelector);
         expect(noindicator.exists()).toBeFalsy();

From 255a2d94255b16db44adfdeee0cb00d1941de06e Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:13:08 +0100
Subject: [PATCH 048/131] fix workflow attributes test

---
 .../{Attributes.test.js => WorkflowAttributes.test.js}     | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
 rename client/src/components/Workflow/Editor/{Attributes.test.js => WorkflowAttributes.test.js} (94%)

diff --git a/client/src/components/Workflow/Editor/Attributes.test.js b/client/src/components/Workflow/Editor/WorkflowAttributes.test.js
similarity index 94%
rename from client/src/components/Workflow/Editor/Attributes.test.js
rename to client/src/components/Workflow/Editor/WorkflowAttributes.test.js
index b1b4e2cd5a4a..8f74ae81d869 100644
--- a/client/src/components/Workflow/Editor/Attributes.test.js
+++ b/client/src/components/Workflow/Editor/WorkflowAttributes.test.js
@@ -3,9 +3,10 @@ import { isDate } from "date-fns";
 
 import { useUserTagsStore } from "@/stores/userTagsStore";
 
-import Attributes from "./Attributes";
 import { UntypedParameters } from "./modules/parameters";
 
+import WorkflowAttributes from "./WorkflowAttributes.vue";
+
 jest.mock("app");
 
 const TEST_ANNOTATION = "my cool annotation";
@@ -24,13 +25,13 @@ useUserTagsStore.mockReturnValue({
     onMultipleNewTagsSeen: jest.fn(),
 });
 
-describe("Attributes", () => {
+describe("WorkflowAttributes", () => {
     it("test attributes", async () => {
         const localVue = createLocalVue();
         const untypedParameters = new UntypedParameters();
         untypedParameters.getParameter("workflow_parameter_0");
         untypedParameters.getParameter("workflow_parameter_1");
-        const wrapper = mount(Attributes, {
+        const wrapper = mount(WorkflowAttributes, {
             propsData: {
                 id: "workflow_id",
                 name: TEST_NAME,

From 033f86fb60c470a24c377a3f853411e0af997d53 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:20:23 +0100
Subject: [PATCH 049/131] fxi activity store test

---
 client/src/stores/activityStore.test.ts | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/client/src/stores/activityStore.test.ts b/client/src/stores/activityStore.test.ts
index b37e5546d1fe..c6c8461c4f2d 100644
--- a/client/src/stores/activityStore.test.ts
+++ b/client/src/stores/activityStore.test.ts
@@ -4,7 +4,7 @@ import { useActivityStore } from "@/stores/activityStore";
 
 // mock Galaxy object
 jest.mock("./activitySetup", () => ({
-    Activities: [
+    defaultActivities: [
         {
             anonymous: false,
             description: "a-description",
@@ -55,16 +55,16 @@ describe("Activity Store", () => {
         setActivePinia(createPinia());
     });
 
-    it("initialize store", () => {
+    it("initialize store", async () => {
         const activityStore = useActivityStore("default");
         expect(activityStore.getAll().length).toBe(0);
-        activityStore.sync();
+        await activityStore.sync();
         expect(activityStore.getAll().length).toBe(1);
     });
 
-    it("add activity", () => {
+    it("add activity", async () => {
         const activityStore = useActivityStore("default");
-        activityStore.sync();
+        await activityStore.sync();
         const initialActivities = activityStore.getAll();
         expect(initialActivities[0]?.visible).toBeTruthy();
         activityStore.setAll(newActivities);
@@ -72,7 +72,7 @@ describe("Activity Store", () => {
         const currentActivities = activityStore.getAll();
         expect(currentActivities[0]).toEqual(newActivities[0]);
         expect(currentActivities[1]).toEqual(newActivities[1]);
-        activityStore.sync();
+        await activityStore.sync();
         const syncActivities = activityStore.getAll();
         expect(syncActivities.length).toEqual(2);
         expect(syncActivities[0]?.description).toEqual("a-description");
@@ -80,19 +80,19 @@ describe("Activity Store", () => {
         expect(syncActivities[1]).toEqual(newActivities[1]);
     });
 
-    it("remove activity", () => {
+    it("remove activity", async () => {
         const activityStore = useActivityStore("default");
-        activityStore.sync();
+        await activityStore.sync();
         const initialActivities = activityStore.getAll();
         expect(initialActivities.length).toEqual(1);
         activityStore.remove("a-id");
         expect(activityStore.getAll().length).toEqual(0);
-        activityStore.sync();
+        await activityStore.sync();
         expect(activityStore.getAll().length).toEqual(1);
         activityStore.setAll(newActivities);
         expect(activityStore.getAll().length).toEqual(2);
         activityStore.remove("b-id");
-        activityStore.sync();
+        await activityStore.sync();
         expect(activityStore.getAll().length).toEqual(1);
     });
 });

From 81a4b9551ca4c78814af0461e9f41d7a9f24f0be Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:41:31 +0100
Subject: [PATCH 050/131] fix empty divs rendered

---
 .../ActivityBar/ActivitySettings.vue          | 105 +++++++++---------
 1 file changed, 52 insertions(+), 53 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivitySettings.vue b/client/src/components/ActivityBar/ActivitySettings.vue
index 36aec9e0c220..d0c8943641ad 100644
--- a/client/src/components/ActivityBar/ActivitySettings.vue
+++ b/client/src/components/ActivityBar/ActivitySettings.vue
@@ -3,7 +3,6 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 import { faSquare, faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
 import { faCheckSquare, faStar, faThumbtack, faTrash } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { storeToRefs } from "pinia";
 import { computed, type ComputedRef } from "vue";
 import { useRouter } from "vue-router/composables";
 
@@ -28,12 +27,13 @@ const emit = defineEmits<{
 }>();
 
 const activityStore = useActivityStore(props.activityBarId);
-const { activities } = storeToRefs(activityStore);
+
+const optionalActivities = computed(() => activityStore.activities.filter((a) => a.optional));
 
 const filteredActivities = computed(() => {
     if (props.query?.length > 0) {
         const queryLower = props.query.toLowerCase();
-        const results = activities.value.filter((a: Activity) => {
+        const results = optionalActivities.value.filter((a: Activity) => {
             const attributeValues = [a.title, a.description];
             for (const value of attributeValues) {
                 if (value.toLowerCase().indexOf(queryLower) !== -1) {
@@ -44,7 +44,7 @@ const filteredActivities = computed(() => {
         });
         return results;
     } else {
-        return activities.value;
+        return optionalActivities.value;
     }
 });
 
@@ -82,56 +82,55 @@ function executeActivity(activity: Activity) {
 <template>
     <div class="activity-settings rounded no-highlight">
         <div v-if="foundActivities" class="activity-settings-content">
-            <div v-for="activity in filteredActivities" :key="activity.id">
-                <button
-                    v-if="activity.optional"
-                    class="activity-settings-item p-2 cursor-pointer"
-                    @click="executeActivity(activity)">
-                    <div class="d-flex justify-content-between align-items-start">
-                        <span class="d-flex justify-content-between w-100">
-                            <span>
-                                <icon class="mr-1" :icon="activity.icon" />
-                                <span v-localize class="font-weight-bold">{{
-                                    activity.title || "No title available"
-                                }}</span>
-                            </span>
-                            <div>
-                                <BButton
-                                    v-if="activity.mutable"
-                                    v-b-tooltip.hover
-                                    data-description="delete activity"
-                                    size="sm"
-                                    title="Delete Activity"
-                                    variant="link"
-                                    @click.stop="onRemove(activity)">
-                                    <FontAwesomeIcon icon="fa-trash" fa-fw />
-                                </BButton>
-                                <BButton
-                                    v-if="activity.visible"
-                                    v-b-tooltip.hover
-                                    size="sm"
-                                    title="Hide in Activity Bar"
-                                    variant="link"
-                                    @click.stop="onFavorite(activity)">
-                                    <FontAwesomeIcon icon="fas fa-star" fa-fw />
-                                </BButton>
-                                <BButton
-                                    v-else
-                                    v-b-tooltip.hover
-                                    size="sm"
-                                    title="Show in Activity Bar"
-                                    variant="link"
-                                    @click.stop="onFavorite(activity)">
-                                    <FontAwesomeIcon icon="far fa-star" fa-fw />
-                                </BButton>
-                            </div>
+            <button
+                v-for="activity in filteredActivities"
+                :key="activity.id"
+                class="activity-settings-item p-2 cursor-pointer"
+                @click="executeActivity(activity)">
+                <div class="d-flex justify-content-between align-items-start">
+                    <span class="d-flex justify-content-between w-100">
+                        <span>
+                            <icon class="mr-1" :icon="activity.icon" />
+                            <span v-localize class="font-weight-bold">{{
+                                activity.title || "No title available"
+                            }}</span>
                         </span>
-                    </div>
-                    <div v-localize class="text-muted">
-                        {{ activity.description || "No description available" }}
-                    </div>
-                </button>
-            </div>
+                        <div>
+                            <BButton
+                                v-if="activity.mutable"
+                                v-b-tooltip.hover
+                                data-description="delete activity"
+                                size="sm"
+                                title="Delete Activity"
+                                variant="link"
+                                @click.stop="onRemove(activity)">
+                                <FontAwesomeIcon icon="fa-trash" fa-fw />
+                            </BButton>
+                            <BButton
+                                v-if="activity.visible"
+                                v-b-tooltip.hover
+                                size="sm"
+                                title="Hide in Activity Bar"
+                                variant="link"
+                                @click.stop="onFavorite(activity)">
+                                <FontAwesomeIcon icon="fas fa-star" fa-fw />
+                            </BButton>
+                            <BButton
+                                v-else
+                                v-b-tooltip.hover
+                                size="sm"
+                                title="Show in Activity Bar"
+                                variant="link"
+                                @click.stop="onFavorite(activity)">
+                                <FontAwesomeIcon icon="far fa-star" fa-fw />
+                            </BButton>
+                        </div>
+                    </span>
+                </div>
+                <div v-localize class="text-muted">
+                    {{ activity.description || "No description available" }}
+                </div>
+            </button>
         </div>
         <div v-else class="activity-settings-content">
             <b-alert v-localize class="py-1 px-2" show> No matching activities found. </b-alert>

From cd5ef662b7f3e5766293095444a89a1862b6a186 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:51:23 +0100
Subject: [PATCH 051/131] fix activity settings test

---
 .../components/ActivityBar/ActivitySettings.test.js | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivitySettings.test.js b/client/src/components/ActivityBar/ActivitySettings.test.js
index fc80db0b912e..873bdd958458 100644
--- a/client/src/components/ActivityBar/ActivitySettings.test.js
+++ b/client/src/components/ActivityBar/ActivitySettings.test.js
@@ -2,8 +2,9 @@ import { createTestingPinia } from "@pinia/testing";
 import { mount } from "@vue/test-utils";
 import { PiniaVuePlugin } from "pinia";
 import { getLocalVue } from "tests/jest/helpers";
+import { nextTick } from "vue";
 
-import { Activities } from "@/stores/activitySetup";
+import { defaultActivities } from "@/stores/activitySetup";
 import { useActivityStore } from "@/stores/activityStore";
 
 import mountTarget from "./ActivitySettings.vue";
@@ -40,30 +41,30 @@ describe("ActivitySettings", () => {
 
     beforeEach(async () => {
         const pinia = createTestingPinia({ stubActions: false });
-        activityStore = useActivityStore("default");
-        activityStore.sync();
+        activityStore = useActivityStore(undefined);
         wrapper = mount(mountTarget, {
             localVue,
             pinia,
             props: {
                 query: "",
-                activityBarScope: "default",
+                activityBarId: undefined,
             },
             stubs: {
                 icon: { template: "<div></div>" },
             },
         });
+        await activityStore.sync();
     });
 
     it("availability of built-in activities", async () => {
         const items = wrapper.findAll(activityItemSelector);
-        const nOptional = Activities.filter((x) => x.optional).length;
+        const nOptional = defaultActivities.filter((x) => x.optional).length;
         expect(items.length).toBe(nOptional);
     });
 
     it("visible and optional activity", async () => {
         activityStore.setAll([testActivity("1")]);
-        await wrapper.vm.$nextTick();
+        await nextTick();
         const items = wrapper.findAll(activityItemSelector);
         expect(items.length).toBe(1);
         const checkbox = items.at(0).find("[title='Hide in Activity Bar']");

From 7b96c14e6ed88a8616c1af14d5776ae6aca5a1e7 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:56:05 +0100
Subject: [PATCH 052/131] fix lint test

---
 .../components/Workflow/Editor/Lint.test.js   | 26 ++++++-------------
 .../src/components/Workflow/Editor/Lint.vue   |  2 +-
 2 files changed, 9 insertions(+), 19 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Lint.test.js b/client/src/components/Workflow/Editor/Lint.test.js
index 6c8e516d428f..83b749fd4f5c 100644
--- a/client/src/components/Workflow/Editor/Lint.test.js
+++ b/client/src/components/Workflow/Editor/Lint.test.js
@@ -111,28 +111,18 @@ describe("Lint", () => {
 
     it("test checked vs unchecked issues", async () => {
         const checked = wrapper.findAll("[data-icon='check']");
-        // Expecting 5 checks:
-        // 1. Workflow is annotated
-        // 2. Non-optional inputs (if available) are formal inputs
-        // 3. Inputs (if available) have labels and annotations
         expect(checked.length).toBe(2);
+
         const unchecked = wrapper.findAll("[data-icon='exclamation-triangle']");
-        // Expecting 3 warnings:
-        // 1. Workflow creator is not specified
-        // 2. Workflow license is not specified
-        // 3. Workflow has no labeled outputs
-        // 4. Untyped parameter found
-        // 5. Missing an annotation
-        // 6. Unlabeled output found
         expect(unchecked.length).toBe(5);
+
         const links = wrapper.findAll("a");
-        expect(links.length).toBe(6);
-        expect(links.at(0).text()).toContain("Try to automatically fix issues.");
-        expect(links.at(1).text()).toContain("Provide Creator Details.");
-        expect(links.at(2).text()).toContain("Specify a License.");
-        expect(links.at(3).text()).toContain("untyped_parameter");
-        expect(links.at(4).text()).toContain("data input: Missing an annotation");
-        expect(links.at(5).text()).toContain("step label: output");
+        expect(links.length).toBe(5);
+        expect(links.at(0).text()).toContain("Provide Creator Details.");
+        expect(links.at(1).text()).toContain("Specify a License.");
+        expect(links.at(2).text()).toContain("untyped_parameter");
+        expect(links.at(3).text()).toContain("data input: Missing an annotation");
+        expect(links.at(4).text()).toContain("step label: output");
     });
 
     it("should fire refactor event to extract untyped parameter and remove unlabeled workflows", async () => {
diff --git a/client/src/components/Workflow/Editor/Lint.vue b/client/src/components/Workflow/Editor/Lint.vue
index 4638398ffd79..e5e495698a3c 100644
--- a/client/src/components/Workflow/Editor/Lint.vue
+++ b/client/src/components/Workflow/Editor/Lint.vue
@@ -71,7 +71,6 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 import { faExclamationTriangle, faMagic } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import BootstrapVue from "bootstrap-vue";
-import LintSection from "components/Workflow/Editor/LintSection";
 import { UntypedParameters } from "components/Workflow/Editor/modules/parameters";
 import { storeToRefs } from "pinia";
 import Vue from "vue";
@@ -91,6 +90,7 @@ import {
 } from "./modules/linting";
 
 import ActivityPanel from "@/components/Panels/ActivityPanel.vue";
+import LintSection from "@/components/Workflow/Editor/LintSection.vue";
 
 Vue.use(BootstrapVue);
 

From b761aa414449e362d4c5564884f90596e495b8c7 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:59:51 +0100
Subject: [PATCH 053/131] fix filter conversion test

---
 client/src/utils/filterConversion.test.js | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/client/src/utils/filterConversion.test.js b/client/src/utils/filterConversion.test.js
index 6867c20c23f0..84affdcae555 100644
--- a/client/src/utils/filterConversion.test.js
+++ b/client/src/utils/filterConversion.test.js
@@ -1,9 +1,9 @@
-import { HistoryFilters } from "components/History/HistoryFilters";
-import { WorkflowFilters } from "components/Workflow/List/WorkflowFilters";
+import { HistoryFilters } from "@/components/History/HistoryFilters";
+import { getWorkflowFilters } from "@/components/Workflow/List/workflowFilters";
 
 describe("test filtering helpers to convert filters to filter text", () => {
-    const MyWorkflowFilters = WorkflowFilters("my");
-    const PublishedWorkflowFilters = WorkflowFilters("published");
+    const MyWorkflowFilters = getWorkflowFilters("my");
+    const PublishedWorkflowFilters = getWorkflowFilters("published");
     it("conversion from filters to new filter text", async () => {
         const normalized = HistoryFilters.defaultFilters;
         expect(Object.keys(normalized).length).toBe(2);
@@ -66,7 +66,7 @@ describe("test filtering helpers to convert filters to filter text", () => {
 });
 
 describe("test filtering helpers to convert filter text to filters", () => {
-    const PublishedWorkflowFilters = WorkflowFilters("published");
+    const PublishedWorkflowFilters = getWorkflowFilters("published");
     function getFilters(filteringClass, filterText) {
         return filteringClass.getValidFilters(Object.fromEntries(filteringClass.getFiltersForText(filterText)))
             .validFilters;

From 02be1476ca6737c9fd279cc85e09a597c7897481 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 16:07:11 +0100
Subject: [PATCH 054/131] fix workflow filters import

---
 client/src/components/Common/FilterMenu.test.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Common/FilterMenu.test.ts b/client/src/components/Common/FilterMenu.test.ts
index 97aaa72cd4da..6f4624b2b0b3 100644
--- a/client/src/components/Common/FilterMenu.test.ts
+++ b/client/src/components/Common/FilterMenu.test.ts
@@ -4,7 +4,7 @@ import { mount, type Wrapper } from "@vue/test-utils";
 
 import { HistoryFilters } from "@/components/History/HistoryFilters";
 import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
-import { WorkflowFilters } from "@/components/Workflow/List/workflowFilters";
+import { getWorkflowFilters } from "@/components/Workflow/List/workflowFilters";
 import Filtering, { compare, contains, equals, toBool, toDate } from "@/utils/filtering";
 
 import FilterMenu from "./FilterMenu.vue";
@@ -306,7 +306,7 @@ describe("FilterMenu", () => {
      * class, ensuring the default values are reflected in the radio-group buttons
      */
     it("test compact menu with checkbox filters on WorkflowFilters", async () => {
-        const myWorkflowFilters = WorkflowFilters("my");
+        const myWorkflowFilters = getWorkflowFilters("my");
         setUpWrapper("Workflows", "search workflows", myWorkflowFilters);
         // a compact `FilterMenu` only needs to be opened once (doesn't toggle out automatically)
         await wrapper.setProps({ showAdvanced: true, view: "compact" });

From f4c31c155d4a92c97e491eb54465914fd28d73bc Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 28 Oct 2024 16:19:09 +0100
Subject: [PATCH 055/131] remove broken test case

---
 .../src/components/Common/FilterMenu.test.ts  | 42 -------------------
 1 file changed, 42 deletions(-)

diff --git a/client/src/components/Common/FilterMenu.test.ts b/client/src/components/Common/FilterMenu.test.ts
index 6f4624b2b0b3..67d9bcc36f58 100644
--- a/client/src/components/Common/FilterMenu.test.ts
+++ b/client/src/components/Common/FilterMenu.test.ts
@@ -201,48 +201,6 @@ describe("FilterMenu", () => {
         );
     });
 
-    it("test buttons that navigate menu and keyup.enter/esc events", async () => {
-        setUpWrapper("Test Items", "search test items", TestFilters);
-
-        expect(wrapper.find("[data-description='advanced filters']").exists()).toBe(false);
-        await wrapper.setProps({ showAdvanced: true });
-        expect(wrapper.find("[data-description='advanced filters']").exists()).toBe(true);
-
-        // only add name filter in the advanced menu
-        let filterName = wrapper.find("[placeholder='any name']");
-        if (filterName.vm && filterName.props().type == "text") {
-            await filterName.setValue("sample name");
-        }
-
-        // -------- Test keyup.enter key:  ---------
-        // toggles view out and performs a search
-        await filterName.trigger("keyup.enter");
-        await expectCorrectEmits("name:'sample name'", TestFilters, false);
-
-        // Test: clearing the filterText
-        const clearButton = wrapper.find("[data-description='reset query']");
-        await clearButton.trigger("click");
-        await expectCorrectEmits("", TestFilters, false);
-
-        // Test: toggling view back in
-        const toggleButton = wrapper.find("[data-description='toggle advanced search']");
-        await toggleButton.trigger("click");
-        await expectCorrectEmits("", TestFilters, true);
-
-        // -------- Test keyup.esc key:  ---------
-        // toggles view out only (doesn't cause a new search / doesn't emulate enter)
-
-        // find name field again (destroyed because of toggling out) and set value
-        filterName = wrapper.find("[placeholder='any name']");
-        if (filterName.vm && filterName.props().type == "text") {
-            filterName.setValue("newnamefilter");
-        }
-
-        // press esc key from name field (should not change emitted filterText unlike enter key)
-        await filterName.trigger("keyup.esc");
-        await expectCorrectEmits("", TestFilters, false);
-    });
-
     /**
      * Testing the default values of the filters defined in the HistoryFilters: Filtering
      * class, ensuring the default values are reflected in the radio-group buttons

From 5dbe5e871d0c4e2193b30f7c2aaab9f485a56440 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 29 Oct 2024 17:11:02 +0100
Subject: [PATCH 056/131] remove workflows section in tool box

---
 client/src/components/Panels/ToolBox.vue | 75 ------------------------
 1 file changed, 75 deletions(-)

diff --git a/client/src/components/Panels/ToolBox.vue b/client/src/components/Panels/ToolBox.vue
index 3589d51b1ab7..50e5aeb10ac7 100644
--- a/client/src/components/Panels/ToolBox.vue
+++ b/client/src/components/Panels/ToolBox.vue
@@ -6,12 +6,9 @@ import { storeToRefs } from "pinia";
 import { computed, type ComputedRef, type PropType, type Ref, ref } from "vue";
 import { useRouter } from "vue-router/composables";
 
-import { getGalaxyInstance } from "@/app";
 import { useGlobalUploadModal } from "@/composables/globalUploadModal";
-import { getAppRoot } from "@/onload/loadConfig";
 import { type Tool, type ToolSection as ToolSectionType } from "@/stores/toolStore";
 import { useToolStore } from "@/stores/toolStore";
-import { type Workflow, type Workflow as WorkflowType } from "@/stores/workflowStore";
 import localize from "@/utils/localization";
 
 import { filterTools, getValidPanelItems, getValidToolsInCurrentView, getValidToolsInEachSection } from "./utilities";
@@ -29,8 +26,6 @@ const emit = defineEmits<{
     (e: "update:panel-query", query: string): void;
     (e: "onInsertTool", toolId: string, toolName: string): void;
     (e: "onInsertModule", moduleName: string, moduleTitle: string | undefined): void;
-    (e: "onInsertWorkflow", workflowLatestId: string | undefined, workflowName: string): void;
-    (e: "onInsertWorkflowSteps", workflowId: string, workflowStepCount: number | undefined): void;
 }>();
 
 const props = defineProps({
@@ -132,44 +127,6 @@ const localPanel: ComputedRef<Record<string, Tool | ToolSectionType> | null> = c
     }
 });
 
-const favWorkflows = computed(() => {
-    const Galaxy = getGalaxyInstance();
-    const storedWorkflowMenuEntries = Galaxy && Galaxy.config.stored_workflow_menu_entries;
-    if (storedWorkflowMenuEntries) {
-        const returnedWfs = [];
-        if (!props.workflow) {
-            returnedWfs.push({
-                title: localize("All workflows") as string,
-                href: `${getAppRoot()}workflows/list`,
-                id: "list",
-            });
-        }
-        const storedWfs = [
-            ...storedWorkflowMenuEntries.map((menuEntry: Workflow) => {
-                return {
-                    id: menuEntry.id,
-                    title: menuEntry.name,
-                    href: `${getAppRoot()}workflows/run?id=${menuEntry.id}`,
-                };
-            }),
-        ];
-        return returnedWfs.concat(storedWfs);
-    } else {
-        return [];
-    }
-});
-
-const workflowSection = computed(() => {
-    if (props.workflow && props.editorWorkflows.length > 0) {
-        return {
-            name: localize("Workflows"),
-            elems: props.workflow && props.editorWorkflows,
-        };
-    } else {
-        return null;
-    }
-});
-
 const buttonIcon = computed(() => (showSections.value ? faEyeSlash : faEye));
 const buttonText = computed(() => (showSections.value ? localize("Hide Sections") : localize("Show Sections")));
 
@@ -178,15 +135,6 @@ function onInsertModule(module: Record<string, any>, event: Event) {
     emit("onInsertModule", module.name, module.title);
 }
 
-function onInsertWorkflow(workflow: WorkflowType, event: Event) {
-    event.preventDefault();
-    emit("onInsertWorkflow", workflow.latest_id, workflow.name);
-}
-
-function onInsertWorkflowSteps(workflow: WorkflowType) {
-    emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
-}
-
 function onToolClick(tool: Tool, evt: Event) {
     if (!props.workflow) {
         if (tool.id === "upload1") {
@@ -309,29 +257,6 @@ function onToggle() {
                             @onFilter="onSectionFilter" />
                     </div>
                 </div>
-                <ToolSection
-                    v-if="props.workflow && workflowSection"
-                    :key="workflowSection.name"
-                    :category="workflowSection"
-                    section-name="workflows"
-                    :sort-items="false"
-                    operation-icon="fa fa-files-o"
-                    operation-title="Insert individual steps."
-                    :query-filter="queryFilter || undefined"
-                    :disable-filter="true"
-                    @onClick="onInsertWorkflow"
-                    @onOperation="onInsertWorkflowSteps" />
-                <div v-else-if="favWorkflows.length > 0">
-                    <ToolSection :category="{ text: 'Workflows' }" />
-                    <div id="internal-workflows" class="toolSectionBody">
-                        <div class="toolSectionBg" />
-                        <div v-for="wf in favWorkflows" :key="wf.id" class="toolTitle">
-                            <a class="title-link" href="javascript:void(0)" @click="router.push(wf.href)">{{
-                                wf.title
-                            }}</a>
-                        </div>
-                    </div>
-                </div>
             </div>
         </div>
     </div>

From 1abe12416277b411fe3862efd9c3d589629d48c3 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 30 Oct 2024 15:36:30 +0100
Subject: [PATCH 057/131] add back activity item id

---
 client/src/components/ActivityBar/ActivityItem.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index 20c040dee10e..e715742a15b1 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -61,6 +61,7 @@ function onClick(evt: MouseEvent): void {
     <Popper reference-is="span" popper-is="span" :placement="tooltipPlacement">
         <template v-slot:reference>
             <b-nav-item
+                :id="id"
                 class="activity-item position-relative my-1 p-2"
                 :class="{ 'nav-item-active': isActive }"
                 :link-classes="`variant-${props.variant}`"

From b2e00515b52dacf788e48965872f387d749bed20 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 31 Oct 2024 17:28:10 +0100
Subject: [PATCH 058/131] make workflow editor default to attributes

---
 client/src/components/ActivityBar/ActivityBar.vue | 6 ++++++
 client/src/components/Workflow/Editor/Index.vue   | 1 +
 2 files changed, 7 insertions(+)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 1c699ea42e78..d026d531a1f8 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -36,6 +36,7 @@ const props = withDefaults(
         optionsHeading?: string;
         optionsIcon?: IconDefinition;
         optionsSearchPlaceholder?: string;
+        initialActivity?: string;
     }>(),
     {
         defaultActivities: undefined,
@@ -47,6 +48,7 @@ const props = withDefaults(
         optionsIcon: () => faEllipsisH,
         optionsSearchPlaceholder: "Search Activities",
         optionsTooltip: "View additional activities",
+        initialActivity: undefined,
     }
 );
 
@@ -61,6 +63,10 @@ const userStore = useUserStore();
 const eventStore = useEventStore();
 const activityStore = useActivityStore(props.activityBarId);
 
+if (props.initialActivity) {
+    activityStore.toggledSideBar = props.initialActivity;
+}
+
 watchImmediate(
     () => props.defaultActivities,
     (defaults) => {
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index c087a21f99c9..e431af478b20 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -37,6 +37,7 @@
             options-heading="Workflow Options"
             options-tooltip="View additional workflow options"
             options-search-placeholder="Search options"
+            initial-activity="workflow-editor-attributes"
             :options-icon="faCog"
             @activityClicked="onActivityClicked">
             <template v-slot:side-panel="{ isActiveSideBar }">

From c06c0a325434da71e3866d83fe88223ee1e825ef Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 4 Nov 2024 16:59:02 +0100
Subject: [PATCH 059/131] add class for editor save button

---
 client/src/components/Workflow/Editor/Index.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index e431af478b20..22e5e1030f77 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -121,7 +121,7 @@
                             <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
                             <b-button
                                 v-if="hasChanges"
-                                class="py-1 px-2"
+                                class="py-1 px-2 editor-button-save"
                                 variant="link"
                                 :title="saveWorkflowTitle"
                                 @click="saveOrCreate">

From 33f4c38e3973b8189a81e9e20260e9984e2f13b5 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 4 Nov 2024 18:11:16 +0100
Subject: [PATCH 060/131] move selector to id

---
 client/src/components/Workflow/Editor/Index.vue |  3 ++-
 client/src/utils/navigation/navigation.yml      | 10 +++++-----
 lib/galaxy/selenium/navigates_galaxy.py         |  4 ++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 22e5e1030f77..cbc429d5de1a 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -121,7 +121,8 @@
                             <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
                             <b-button
                                 v-if="hasChanges"
-                                class="py-1 px-2 editor-button-save"
+                                id="workflow-save-button"
+                                class="py-1 px-2"
                                 variant="link"
                                 :title="saveWorkflowTitle"
                                 @click="saveOrCreate">
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index dc4fcaeaa0cb..801bdf3b2ea0 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -92,10 +92,10 @@ object_store_instances:
       create_button: '#object-store-create'
       _: '#user-object-stores-index'
 
-  create: 
+  create:
     selectors:
       select: '.object-store-template-select-button[data-template-id="${template_id}"]'
-      _: '#create-object-store-landing'      
+      _: '#create-object-store-landing'
       submit: '#submit'
 
 file_source_instances:
@@ -104,10 +104,10 @@ file_source_instances:
       create_button: '#file-source-create'
       _: '#user-file-sources-index'
 
-  create: 
+  create:
     selectors:
       select: '.file-source-template-select-button[data-template-id="${template_id}"]'
-      _: '#create-file-source-landing'      
+      _: '#create-file-source-landing'
       submit: '#submit'
 
 toolbox_filters:
@@ -830,7 +830,7 @@ workflow_editor:
     tool_version_button: ".tool-versions"
     connector_for: "#connection-${sink_id}-${source_id}"
     connector_invalid_for: "#connection-${sink_id}-${source_id} .connection.invalid"
-    save_button: '.editor-button-save'
+    save_button: '#workflow-save-button'
     save_workflow_confirmation_button: '#save-workflow-confirmation .btn-primary'
     state_modal_body: '.state-upgrade-modal'
     modal_button_continue: '.modal-footer .btn'
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 7a1c2aab576c..34627ca86501 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1231,10 +1231,10 @@ def workflow_editor_click_option(self, option_label):
             raise Exception(f"Failed to find workflow editor option with label [{option_label}]")
 
     def workflow_editor_click_options(self):
-        return self.wait_for_and_click_selector("#workflow-options-button")
+        return self.wait_for_and_click_selector("#activity-settings")
 
     def workflow_editor_options_menu_element(self):
-        return self.wait_for_selector_visible("#workflow-options-button")
+        return self.wait_for_selector_visible("#activity-settings")
 
     def workflow_editor_click_run(self):
         return self.wait_for_and_click_selector("#workflow-run-button")

From daad64d6c0aab301f830eb384bab6ddf8b21452a Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 4 Nov 2024 18:46:14 +0100
Subject: [PATCH 061/131] fix auto layout

---
 lib/galaxy/selenium/navigates_galaxy.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 34627ca86501..79d3e9e7640c 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1214,7 +1214,7 @@ def workflow_editor_set_license(self, license: str) -> None:
     def workflow_editor_click_option(self, option_label):
         self.workflow_editor_click_options()
         menu_element = self.workflow_editor_options_menu_element()
-        option_elements = menu_element.find_elements(By.CSS_SELECTOR, "a")
+        option_elements = menu_element.find_elements(By.CSS_SELECTOR, "button")
         assert len(option_elements) > 0, "Failed to find workflow editor options"
         self.sleep_for(WAIT_TYPES.UX_RENDER)
         found_option = False
@@ -1234,7 +1234,7 @@ def workflow_editor_click_options(self):
         return self.wait_for_and_click_selector("#activity-settings")
 
     def workflow_editor_options_menu_element(self):
-        return self.wait_for_selector_visible("#activity-settings")
+        return self.wait_for_selector_visible(".activity-settings")
 
     def workflow_editor_click_run(self):
         return self.wait_for_and_click_selector("#workflow-run-button")

From 98baca51a4c97193372fb8e6e6231e2caf24bfee Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 4 Nov 2024 20:05:18 +0100
Subject: [PATCH 062/131] add button titles to node inspector

---
 client/src/components/Workflow/Editor/NodeInspector.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue
index 2013b720985f..af19ab5cd1bf 100644
--- a/client/src/components/Workflow/Editor/NodeInspector.vue
+++ b/client/src/components/Workflow/Editor/NodeInspector.vue
@@ -70,6 +70,7 @@ function close() {
                     class="heading-button"
                     variant="link"
                     size="md"
+                    title="maximize"
                     @click="inspectorStore.setMaximized(props.step, true)">
                     <ArrowLeftFromLine absolute-stroke-width :size="17" />
                 </BButton>
@@ -78,6 +79,7 @@ function close() {
                     class="heading-button"
                     variant="link"
                     size="md"
+                    title="minimize"
                     @click="inspectorStore.setMaximized(props.step, false)">
                     <ArrowRightToLine absolute-stroke-width :size="17" />
                 </BButton>
@@ -100,7 +102,7 @@ function close() {
                     </BDropdownItemButton>
                 </BDropdown>
 
-                <BButton class="heading-button" variant="link" size="md" @click="close">
+                <BButton class="heading-button" variant="link" size="md" title="close" @click="close">
                     <FontAwesomeIcon :icon="faTimes" fixed-width />
                 </BButton>
             </BButtonGroup>

From 70391a923a6adf35f81a10f76ad12863e283f365 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 4 Nov 2024 20:31:33 +0100
Subject: [PATCH 063/131] fix maximize center pane function

---
 client/src/utils/navigation/navigation.yml       |  3 +++
 lib/galaxy_test/selenium/test_workflow_editor.py | 14 ++++++++------
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 801bdf3b2ea0..3d71b874ae14 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -19,6 +19,7 @@ _:  # global stuff
     right_panel_drag: '.flex-panel.right .drag-handle'
     right_panel_collapse: '.collapse-button.right'
     by_attribute: '${scope} [${name}="${value}"]'
+    active_nav_item: '.nav-item-active'
 
     confirm_button:
       type: xpath
@@ -744,6 +745,8 @@ workflow_editor:
       freehand_path: ".freehand-workflow-comment path"
       delete: "button[title='Delete comment']"
   selectors:
+    node_inspector: '.tool-inspector'
+    node_inspector_close: ".tool-inspector [title='close']"
     canvas_body: '#workflow-canvas'
     edit_annotation: '#workflow-annotation'
     edit_name: '#workflow-name'
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index af8ba29fbc53..d07a00d8af51 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -1377,12 +1377,14 @@ def assert_node_output_is(self, label: str, output_type: str, subcollection_type
         self.click_center()
 
     def workflow_editor_maximize_center_pane(self, collapse_left=True, collapse_right=True):
-        if collapse_left:
-            self.hover_over(self.components._.left_panel_drag.wait_for_visible())
-            self.components._.left_panel_collapse.wait_for_and_click()
-        if collapse_right:
-            self.hover_over(self.components._.right_panel_drag.wait_for_visible())
-            self.components._.right_panel_collapse.wait_for_and_click()
+        self.sleep_for(self.wait_types.UX_RENDER)
+        editor = self.components.workflow_editor
+
+        if collapse_right and not editor.node_inspector.is_absent:
+            editor.node_inspector_close.wait_for_and_click()
+        if collapse_left and not self.components._.active_nav_item.is_absent:
+            self.components._.active_nav_item.wait_for_and_click()
+
         self.sleep_for(self.wait_types.UX_RENDER)
 
     def workflow_editor_connect(self, source, sink, screenshot_partial=None):

From a5aeaa7a7c68ac384627a30362276548814420ea Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 5 Nov 2024 18:27:08 +0100
Subject: [PATCH 064/131] make auto layout respect grid and snapping

---
 .../Workflow/Editor/modules/layout.ts         | 82 +++++++++++++------
 1 file changed, 55 insertions(+), 27 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 11a31adf138a..8b9c9ba34354 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -1,49 +1,72 @@
-import ELK from "elkjs/lib/elk.bundled.js";
+import ELK, { type ElkExtendedEdge, type ElkNode } from "elkjs/lib/elk.bundled";
 
 import { useConnectionStore } from "@/stores/workflowConnectionStore";
 import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
+import { useWorkflowEditorToolbarStore } from "@/stores/workflowEditorToolbarStore";
 import { type Step } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
+import { match } from "@/utils/utils";
 
 const elk = new ELK();
 
-interface GraphChild {
-    id: string;
-    height: number;
-    width: number;
-    ports: {
-        id: string;
-        properties: {
-            "port.side": string;
-            "port.index": number;
-        };
-    }[];
+interface OptionObject {
+    [key: string]: OptionValue | OptionObject;
 }
+type OptionValue = number | string | boolean;
+
+export function elkOptionObject(object: OptionObject): string {
+    const entries = Object.entries(object);
+    const stringifiedEntries = entries.map(([key, value]) => {
+        const type = typeof value as "number" | "string" | "boolean" | "object";
+
+        const valueAsString = match(type, {
+            number: () => `${value}`,
+            string: () => `"${value}"`,
+            boolean: () => `${value}`,
+            object: () => elkOptionObject(value as OptionObject),
+        });
+
+        return `${key}=${valueAsString}`;
+    });
 
-interface GraphEdge {
-    id: string;
-    sources: string[];
-    targets: string[];
+    return `[${stringifiedEntries.join(", ")}]`;
 }
 
-interface NewGraph {
-    id: string;
-    layoutOptions: { [index: string]: string };
-    children: GraphChild[];
-    edges: GraphEdge[];
+export function elkSpacing(left = 0, top = 0, right = 0, bottom = 0) {
+    return elkOptionObject({
+        left,
+        top,
+        right,
+        bottom,
+    });
 }
 
 export async function autoLayout(id: string, steps: { [index: string]: Step }) {
     const connectionStore = useConnectionStore(id);
     const stateStore = useWorkflowStateStore(id);
+    const toolbarStore = useWorkflowEditorToolbarStore(id);
+
+    const snappingDistance = Math.max(toolbarStore.snapActive ? toolbarStore.snapDistance : 0, 10);
+    const horizontalDistance = Math.max(snappingDistance * 2, 100);
+    const verticalDistance = Math.max(snappingDistance, 50);
+
+    const roundUpToSnappingDistance = (value: number) => {
+        const floatErrorTolerance = 0.0001;
+        return Math.ceil(value / snappingDistance - floatErrorTolerance) * snappingDistance;
+    };
 
     // Convert this to ELK compat.
-    const newGraph: NewGraph = {
+    const newGraph: ElkNode = {
         id: "",
         layoutOptions: {
             "elk.algorithm": "layered",
+            "elk.padding": elkSpacing(0, 0),
+            "elk.spacing.nodeNode": `${verticalDistance}`,
+            "elk.layered.spacing.baseValue": `${horizontalDistance}`,
             "crossingMinimization.semiInteractive": "true",
-            "nodePlacement.strategy": "NETWORK_SIMPLEX",
+            "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
+            "elk.hierarchyHandling": "INCLUDE_CHILDREN",
+            "elk.alignment": "TOP",
         },
         children: [],
         edges: [],
@@ -75,14 +98,14 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }) {
 
         return {
             id: stepId,
-            height: position.height + 20,
-            width: position.width + 60,
+            height: roundUpToSnappingDistance(position.height),
+            width: roundUpToSnappingDistance(position.width),
             ports: inputs.concat(outputs),
         };
     });
 
     newGraph.edges = connectionStore.connections.map((connection) => {
-        const edge: GraphEdge = {
+        const edge: ElkExtendedEdge = {
             id: `e_${connection.input.stepId}_${connection.output.stepId}`,
             sources: [`${connection.output.stepId}/out/${connection.output.name}`],
             targets: [`${connection.input.stepId}/in/${connection.input.name}`],
@@ -90,11 +113,16 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }) {
         return edge;
     });
 
+    const roundToSnappingDistance = (value: number) => Math.round(value / snappingDistance) * snappingDistance;
+
     try {
         const elkNode = await elk.layout(newGraph);
         // Reapply positions to galaxy graph from our relayed out graph.
         const newSteps = elkNode.children?.map((q) => {
-            const newStep = { ...steps[q.id], position: { top: q.y, left: q.x } };
+            const newStep = {
+                ...steps[q.id],
+                position: { top: roundToSnappingDistance(q.y as number), left: roundToSnappingDistance(q.x as number) },
+            };
             return newStep;
         });
         return newSteps;

From 4346f2d58c87671f4d3c081ab32f09411a775d43 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:14:41 +0100
Subject: [PATCH 065/131] make auto layout an action

---
 .../Workflow/Editor/Actions/stepActions.ts    | 70 ++++++++++++++++++-
 .../src/components/Workflow/Editor/Index.vue  |  9 +--
 .../Workflow/Editor/modules/layout.ts         | 14 ++--
 3 files changed, 78 insertions(+), 15 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts
index 168846c66dfa..107651acbfd1 100644
--- a/client/src/components/Workflow/Editor/Actions/stepActions.ts
+++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts
@@ -4,7 +4,7 @@ import { useRefreshFromStore } from "@/stores/refreshFromStore";
 import { LazyUndoRedoAction, UndoRedoAction, type UndoRedoStore } from "@/stores/undoRedoStore";
 import { type Connection, type WorkflowConnectionStore } from "@/stores/workflowConnectionStore";
 import { type WorkflowStateStore } from "@/stores/workflowEditorStateStore";
-import { type NewStep, type Step, type WorkflowStepStore } from "@/stores/workflowStepStore";
+import { type NewStep, type Step, useWorkflowStepStore, type WorkflowStepStore } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
 
 import { cloneStepWithUniqueLabel, getLabelSet } from "./cloneStep";
@@ -415,6 +415,74 @@ export class ToggleStepSelectedAction extends UndoRedoAction {
     }
 }
 
+export class AutoLayoutAction extends UndoRedoAction {
+    stepStore;
+    positions: { id: string; x: number; y: number }[];
+    oldPositions: { id: string; x: number; y: number }[];
+    workflowId;
+    ran;
+
+    constructor(workflowId: string) {
+        super();
+
+        this.workflowId = workflowId;
+        this.stepStore = useWorkflowStepStore(workflowId);
+        this.positions = [];
+        this.oldPositions = [];
+        this.ran = false;
+    }
+
+    get name() {
+        return "auto layout";
+    }
+
+    private mapPositionsToStore(positions: { id: string; x: number; y: number }[]) {
+        positions.map((p) => {
+            const step = this.stepStore.steps[p.id];
+            if (step) {
+                this.stepStore.updateStep({
+                    ...step,
+                    position: {
+                        top: p.y,
+                        left: p.x,
+                    },
+                });
+            }
+        });
+    }
+
+    async run() {
+        this.ran = true;
+
+        this.oldPositions = Object.values(this.stepStore.steps).map((step) => ({
+            id: `${step.id}`,
+            x: step.position?.left ?? 0,
+            y: step.position?.top ?? 0,
+        }));
+
+        const { autoLayout } = await import(
+            /* webpackChunkName: "workflowLayout" */ "@/components/Workflow/Editor/modules/layout"
+        );
+
+        const newPositions = await autoLayout(this.workflowId, this.stepStore.steps);
+        assertDefined(newPositions);
+        this.positions = newPositions;
+
+        if (this.ran) {
+            this.mapPositionsToStore(this.positions);
+        }
+    }
+
+    undo() {
+        this.ran = false;
+        this.mapPositionsToStore(this.oldPositions);
+    }
+
+    redo() {
+        this.mapPositionsToStore(this.positions);
+    }
+}
+
 export function useStepActions(
     stepStore: WorkflowStepStore,
     undoRedoStore: UndoRedoStore,
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index cbc429d5de1a..128aed42fd62 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -202,7 +202,7 @@ import { LastQueue } from "@/utils/lastQueue";
 import { errorMessageAsString } from "@/utils/simple-error";
 
 import { Services } from "../services";
-import { InsertStepAction, useStepActions } from "./Actions/stepActions";
+import { AutoLayoutAction, InsertStepAction, useStepActions } from "./Actions/stepActions";
 import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
 import { defaultPosition } from "./composables/useDefaultStepPosition";
 import { useSpecialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
@@ -757,11 +757,8 @@ export default {
             }
         },
         onLayout() {
-            return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => {
-                layout.autoLayout(this.id, this.steps).then((newSteps) => {
-                    newSteps.map((step) => this.stepStore.updateStep(step));
-                });
-            });
+            const action = new AutoLayoutAction(this.id);
+            this.undoRedoStore.applyAction(action);
         },
         onAnnotation(nodeId, newAnnotation) {
             this.stepActions.setAnnotation(this.steps[nodeId], newAnnotation);
diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 8b9c9ba34354..bd41a13c076f 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -118,14 +118,12 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }) {
     try {
         const elkNode = await elk.layout(newGraph);
         // Reapply positions to galaxy graph from our relayed out graph.
-        const newSteps = elkNode.children?.map((q) => {
-            const newStep = {
-                ...steps[q.id],
-                position: { top: roundToSnappingDistance(q.y as number), left: roundToSnappingDistance(q.x as number) },
-            };
-            return newStep;
-        });
-        return newSteps;
+        const positions = elkNode.children?.map((q) => ({
+            id: q.id,
+            x: roundToSnappingDistance(q.x as number),
+            y: roundToSnappingDistance(q.y as number),
+        }));
+        return positions;
     } catch (error) {
         console.error(error);
     }

From 75e9b2ddf6985096b1b1a01c0e192ba07b46f4be Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 7 Nov 2024 23:15:17 +0100
Subject: [PATCH 066/131] make activity panel scrollable

---
 client/src/components/Panels/ActivityPanel.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Panels/ActivityPanel.vue b/client/src/components/Panels/ActivityPanel.vue
index 057e09df25f2..20fb01d0d4ca 100644
--- a/client/src/components/Panels/ActivityPanel.vue
+++ b/client/src/components/Panels/ActivityPanel.vue
@@ -77,7 +77,7 @@ const hasGoToAll = computed(() => props.goToAllTitle && props.href);
         display: flex;
         flex-direction: column;
         flex-grow: 1;
-        overflow-y: hidden;
+        overflow-y: auto;
         position: relative;
         button:first-child {
             background: none;

From 46aff8e26b71bda665ede4660027f3ffa433706a Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 11 Nov 2024 20:02:46 +0100
Subject: [PATCH 067/131] close inspector instead of showing attributes

---
 .../Workflow/Editor/Actions/actions.test.ts     |  5 +----
 .../Workflow/Editor/Actions/stepActions.ts      | 17 +++--------------
 client/src/components/Workflow/Editor/Index.vue |  2 +-
 3 files changed, 5 insertions(+), 19 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Actions/actions.test.ts b/client/src/components/Workflow/Editor/Actions/actions.test.ts
index 78290cde330b..08d373cbfe49 100644
--- a/client/src/components/Workflow/Editor/Actions/actions.test.ts
+++ b/client/src/components/Workflow/Editor/Actions/actions.test.ts
@@ -269,11 +269,8 @@ describe("Workflow Undo Redo Actions", () => {
 
         it("RemoveStepAction", () => {
             const step = addStep();
-            const showAttributesCallback = jest.fn();
-            const action = new RemoveStepAction(stepStore, stateStore, connectionStore, showAttributesCallback, step);
+            const action = new RemoveStepAction(stepStore, stateStore, connectionStore, step);
             testUndoRedo(action);
-
-            expect(showAttributesCallback).toBeCalledTimes(2);
         });
 
         it("CopyStepAction", () => {
diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts
index 107651acbfd1..2f5c0614f7f5 100644
--- a/client/src/components/Workflow/Editor/Actions/stepActions.ts
+++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts
@@ -208,14 +208,12 @@ export class UpdateStepAction extends UndoRedoAction {
     run() {
         const step = this.stepStore.getStep(this.stepId);
         assertDefined(step);
-        this.stateStore.activeNodeId = this.stepId;
         this.stepStore.updateStep({ ...step, ...this.toPartial });
     }
 
     undo() {
         const step = this.stepStore.getStep(this.stepId);
         assertDefined(step);
-        this.stateStore.activeNodeId = this.stepId;
         this.stepStore.updateStep({ ...step, ...this.fromPartial });
         this.onUndoRedo?.();
     }
@@ -297,7 +295,6 @@ export class InsertStepAction extends UndoRedoAction {
     redo() {
         this.run();
         assertDefined(this.stepId);
-        this.stateStore.activeNodeId = this.stepId;
     }
 }
 
@@ -305,7 +302,6 @@ export class RemoveStepAction extends UndoRedoAction {
     stepStore;
     stateStore;
     connectionStore;
-    showAttributesCallback;
     step: Step;
     connections: Connection[];
 
@@ -313,14 +309,12 @@ export class RemoveStepAction extends UndoRedoAction {
         stepStore: WorkflowStepStore,
         stateStore: WorkflowStateStore,
         connectionStore: WorkflowConnectionStore,
-        showAttributesCallback: () => void,
         step: Step
     ) {
         super();
         this.stepStore = stepStore;
         this.stateStore = stateStore;
         this.connectionStore = connectionStore;
-        this.showAttributesCallback = showAttributesCallback;
         this.step = structuredClone(step);
         this.connections = structuredClone(this.connectionStore.getConnectionsForStep(this.step.id));
     }
@@ -331,14 +325,13 @@ export class RemoveStepAction extends UndoRedoAction {
 
     run() {
         this.stepStore.removeStep(this.step.id);
-        this.showAttributesCallback();
+        this.stateStore.activeNodeId = null;
         this.stateStore.hasChanges = true;
     }
 
     undo() {
         this.stepStore.addStep(structuredClone(this.step), false, false);
         this.connections.forEach((connection) => this.connectionStore.addConnection(connection));
-        this.stateStore.activeNodeId = this.step.id;
         this.stateStore.hasChanges = true;
     }
 }
@@ -369,7 +362,6 @@ export class CopyStepAction extends UndoRedoAction {
     run() {
         const newStep = this.stepStore.addStep(structuredClone(this.step));
         this.stepId = newStep.id;
-        this.stateStore.activeNodeId = this.stepId;
         this.stateStore.hasChanges = true;
     }
 
@@ -549,7 +541,6 @@ export function useStepActions(
             undoRedoStore.applyLazyAction(action, timeout);
 
             action.onUndoRedo = () => {
-                stateStore.activeNodeId = step.id;
                 stateStore.hasChanges = true;
             };
 
@@ -615,7 +606,6 @@ export function useStepActions(
 
         if (!action.isEmpty()) {
             action.onUndoRedo = () => {
-                stateStore.activeNodeId = from.id;
                 stateStore.hasChanges = true;
                 refresh();
             };
@@ -623,8 +613,8 @@ export function useStepActions(
         }
     }
 
-    function removeStep(step: Step, showAttributesCallback: () => void) {
-        const action = new RemoveStepAction(stepStore, stateStore, connectionStore, showAttributesCallback, step);
+    function removeStep(step: Step) {
+        const action = new RemoveStepAction(stepStore, stateStore, connectionStore, step);
         undoRedoStore.applyAction(action);
     }
 
@@ -641,7 +631,6 @@ export function useStepActions(
 
         if (!action.isEmpty()) {
             action.onUndoRedo = () => {
-                stateStore.activeNodeId = id;
                 stateStore.hasChanges = true;
                 refresh();
             };
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 128aed42fd62..6038631d2aed 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -638,7 +638,7 @@ export default {
             this.stepActions.updateStep(nodeId, partialStep);
         },
         onRemove(nodeId) {
-            this.stepActions.removeStep(this.steps[nodeId], this.showAttributes);
+            this.stepActions.removeStep(this.steps[nodeId]);
         },
         onEditSubworkflow(contentId) {
             const editUrl = `/workflows/edit?workflow_id=${contentId}`;

From 3ff11168f971e05514eaf3885ff6df8d6b9b082e Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 11 Nov 2024 20:07:23 +0100
Subject: [PATCH 068/131] open nodes from lint panel

---
 client/src/components/Workflow/Editor/Lint.vue | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Lint.vue b/client/src/components/Workflow/Editor/Lint.vue
index e5e495698a3c..3cb265c9d90c 100644
--- a/client/src/components/Workflow/Editor/Lint.vue
+++ b/client/src/components/Workflow/Editor/Lint.vue
@@ -50,7 +50,7 @@
             :warning-items="warningMissingMetadata"
             @onMouseOver="onHighlight"
             @onMouseLeave="onUnhighlight"
-            @onClick="onScrollTo" />
+            @onClick="openAndFocus" />
         <LintSection
             success-message="This workflow has outputs and they all have valid labels."
             warning-message="The following workflow outputs have no labels, they should be assigned a useful label or
@@ -131,9 +131,9 @@ export default {
     },
     setup() {
         const stores = useWorkflowStores();
-        const { connectionStore, stepStore } = stores;
+        const { connectionStore, stepStore, stateStore } = stores;
         const { hasActiveOutputs } = storeToRefs(stepStore);
-        return { stores, connectionStore, stepStore, hasActiveOutputs };
+        return { stores, connectionStore, stepStore, hasActiveOutputs, stateStore };
     },
     computed: {
         showRefactor() {
@@ -213,7 +213,8 @@ export default {
                 this.$emit("onScrollTo", item.stepId);
             }
         },
-        onScrollTo(item) {
+        openAndFocus(item) {
+            this.stateStore.activeNodeId = item.stepId;
             this.$emit("onScrollTo", item.stepId);
         },
         onHighlight(item) {

From 7f5bebb7abecd5e6a99969800b0b663852737a4c Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 11 Nov 2024 22:08:38 +0100
Subject: [PATCH 069/131] auto layout frame comments

---
 .../Workflow/Editor/Actions/stepActions.ts    |  49 +++-
 .../Workflow/Editor/modules/layout.ts         | 234 ++++++++++++++----
 2 files changed, 229 insertions(+), 54 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts
index 2f5c0614f7f5..af424a07eb14 100644
--- a/client/src/components/Workflow/Editor/Actions/stepActions.ts
+++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts
@@ -3,6 +3,7 @@ import { useToast } from "@/composables/toast";
 import { useRefreshFromStore } from "@/stores/refreshFromStore";
 import { LazyUndoRedoAction, UndoRedoAction, type UndoRedoStore } from "@/stores/undoRedoStore";
 import { type Connection, type WorkflowConnectionStore } from "@/stores/workflowConnectionStore";
+import { useWorkflowCommentStore } from "@/stores/workflowEditorCommentStore";
 import { type WorkflowStateStore } from "@/stores/workflowEditorStateStore";
 import { type NewStep, type Step, useWorkflowStepStore, type WorkflowStepStore } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
@@ -407,10 +408,16 @@ export class ToggleStepSelectedAction extends UndoRedoAction {
     }
 }
 
+interface Positions {
+    steps: { id: string; x: number; y: number }[];
+    comments: { id: string; x: number; y: number; w: number; h: number }[];
+}
+
 export class AutoLayoutAction extends UndoRedoAction {
     stepStore;
-    positions: { id: string; x: number; y: number }[];
-    oldPositions: { id: string; x: number; y: number }[];
+    commentStore;
+    positions: Positions;
+    oldPositions: Positions;
     workflowId;
     ran;
 
@@ -419,8 +426,18 @@ export class AutoLayoutAction extends UndoRedoAction {
 
         this.workflowId = workflowId;
         this.stepStore = useWorkflowStepStore(workflowId);
-        this.positions = [];
-        this.oldPositions = [];
+        this.commentStore = useWorkflowCommentStore(workflowId);
+
+        this.positions = {
+            steps: [],
+            comments: [],
+        };
+
+        this.oldPositions = {
+            steps: [],
+            comments: [],
+        };
+
         this.ran = false;
     }
 
@@ -428,8 +445,8 @@ export class AutoLayoutAction extends UndoRedoAction {
         return "auto layout";
     }
 
-    private mapPositionsToStore(positions: { id: string; x: number; y: number }[]) {
-        positions.map((p) => {
+    private mapPositionsToStore(positions: Positions) {
+        positions.steps.map((p) => {
             const step = this.stepStore.steps[p.id];
             if (step) {
                 this.stepStore.updateStep({
@@ -441,12 +458,21 @@ export class AutoLayoutAction extends UndoRedoAction {
                 });
             }
         });
+
+        positions.comments.map((c) => {
+            const id = parseInt(c.id, 10);
+            const comment = this.commentStore.commentsRecord[id];
+            if (comment) {
+                this.commentStore.changePosition(id, [c.x, c.y]);
+                this.commentStore.changeSize(id, [c.w, c.h]);
+            }
+        });
     }
 
     async run() {
         this.ran = true;
 
-        this.oldPositions = Object.values(this.stepStore.steps).map((step) => ({
+        this.oldPositions.steps = Object.values(this.stepStore.steps).map((step) => ({
             id: `${step.id}`,
             x: step.position?.left ?? 0,
             y: step.position?.top ?? 0,
@@ -456,9 +482,14 @@ export class AutoLayoutAction extends UndoRedoAction {
             /* webpackChunkName: "workflowLayout" */ "@/components/Workflow/Editor/modules/layout"
         );
 
-        const newPositions = await autoLayout(this.workflowId, this.stepStore.steps);
+        this.commentStore.resolveCommentsInFrames();
+        this.commentStore.resolveStepsInFrames();
+
+        const newPositions = await autoLayout(this.workflowId, this.stepStore.steps, this.commentStore.comments);
+
         assertDefined(newPositions);
-        this.positions = newPositions;
+
+        this.positions = newPositions as Positions;
 
         if (this.ran) {
             this.mapPositionsToStore(this.positions);
diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index bd41a13c076f..e940111f8baf 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -1,8 +1,8 @@
 import ELK, { type ElkExtendedEdge, type ElkNode } from "elkjs/lib/elk.bundled";
 
 import { useConnectionStore } from "@/stores/workflowConnectionStore";
+import type { WorkflowComment } from "@/stores/workflowEditorCommentStore";
 import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
-import { useWorkflowEditorToolbarStore } from "@/stores/workflowEditorToolbarStore";
 import { type Step } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
 import { match } from "@/utils/utils";
@@ -41,12 +41,12 @@ export function elkSpacing(left = 0, top = 0, right = 0, bottom = 0) {
     });
 }
 
-export async function autoLayout(id: string, steps: { [index: string]: Step }) {
+export async function autoLayout(id: string, steps: { [index: string]: Step }, comments: WorkflowComment[]) {
     const connectionStore = useConnectionStore(id);
     const stateStore = useWorkflowStateStore(id);
-    const toolbarStore = useWorkflowEditorToolbarStore(id);
 
-    const snappingDistance = Math.max(toolbarStore.snapActive ? toolbarStore.snapDistance : 0, 10);
+    // making this follow the user set snapping distance get's messy fast, so it's hardcoded for simplicity
+    const snappingDistance = 10;
     const horizontalDistance = Math.max(snappingDistance * 2, 100);
     const verticalDistance = Math.max(snappingDistance, 50);
 
@@ -55,54 +55,29 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }) {
         return Math.ceil(value / snappingDistance - floatErrorTolerance) * snappingDistance;
     };
 
+    const baseLayoutOptions = {
+        "elk.layered.spacing.nodeNodeBetweenLayers": `${horizontalDistance / 2}`,
+    };
+
     // Convert this to ELK compat.
     const newGraph: ElkNode = {
         id: "",
         layoutOptions: {
-            "elk.algorithm": "layered",
+            ...baseLayoutOptions,
             "elk.padding": elkSpacing(0, 0),
-            "elk.spacing.nodeNode": `${verticalDistance}`,
+            "elk.hierarchyHandling": "INCLUDE_CHILDREN",
             "elk.layered.spacing.baseValue": `${horizontalDistance}`,
-            "crossingMinimization.semiInteractive": "true",
+            "elk.algorithm": "layered",
             "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
-            "elk.hierarchyHandling": "INCLUDE_CHILDREN",
+            "elk.spacing.nodeNode": `${verticalDistance}`,
+            "crossingMinimization.semiInteractive": "true",
             "elk.alignment": "TOP",
         },
         children: [],
         edges: [],
     };
 
-    newGraph.children = Object.entries(steps).map(([stepId, step]) => {
-        const inputs = Object.values(step.inputs).map((input) => {
-            return {
-                id: `${stepId}/in/${input.name}`,
-                properties: {
-                    "port.side": "WEST",
-                    "port.index": step.id,
-                },
-            };
-        });
-
-        const outputs = Object.values(step.outputs).map((output) => {
-            return {
-                id: `${stepId}/out/${output.name}`,
-                properties: {
-                    "port.side": "EAST",
-                    "port.index": step.id,
-                },
-            };
-        });
-
-        const position = stateStore.stepPosition[step.id];
-        assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
-
-        return {
-            id: stepId,
-            height: roundUpToSnappingDistance(position.height),
-            width: roundUpToSnappingDistance(position.width),
-            ports: inputs.concat(outputs),
-        };
-    });
+    newGraph.children = graphToElkGraph(steps, comments, stateStore, roundUpToSnappingDistance, baseLayoutOptions);
 
     newGraph.edges = connectionStore.connections.map((connection) => {
         const edge: ElkExtendedEdge = {
@@ -117,14 +92,183 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }) {
 
     try {
         const elkNode = await elk.layout(newGraph);
-        // Reapply positions to galaxy graph from our relayed out graph.
-        const positions = elkNode.children?.map((q) => ({
-            id: q.id,
-            x: roundToSnappingDistance(q.x as number),
-            y: roundToSnappingDistance(q.y as number),
-        }));
+        const positions = graphToPositions(elkNode.children, roundToSnappingDistance);
+        console.log(positions);
         return positions;
     } catch (error) {
         console.error(error);
     }
 }
+
+interface HierarchicalComment {
+    comment: WorkflowComment;
+    root: boolean;
+    children: (HierarchicalComment | Step)[];
+}
+
+function graphToElkGraph(
+    steps: Record<number, Step>,
+    comments: WorkflowComment[],
+    stateStore: ReturnType<typeof useWorkflowStateStore>,
+    roundingFunction: (value: number) => number,
+    layoutOptions: Record<string, string>
+): ElkNode[] {
+    const flatHierarchicalComments: Map<number, HierarchicalComment> = new Map(
+        comments.map((comment) => [comment.id, { comment, root: true, children: [] }])
+    );
+
+    const rootSteps = new Map(Object.entries(steps));
+
+    flatHierarchicalComments.forEach((c) => {
+        if (c.comment.child_comments) {
+            c.comment.child_comments.forEach((id) => {
+                const childComment = flatHierarchicalComments.get(id)!;
+                childComment.root = false;
+                c.children.push(childComment);
+            });
+        }
+
+        if (c.comment.child_steps) {
+            c.comment.child_steps.forEach((id) => {
+                const idAsString = `${id}`;
+                const childStep = rootSteps.get(idAsString)!;
+                rootSteps.delete(idAsString);
+                c.children.push(childStep);
+            });
+        }
+    });
+
+    const rootHierarchicalComments: HierarchicalComment[] = [...flatHierarchicalComments.values()].filter(
+        (c) => c.root
+    );
+
+    const elkRootSteps = [...rootSteps.values()].map((step) => {
+        return stepToElkStep(step, stateStore, roundingFunction);
+    });
+
+    const elkRootComments = rootHierarchicalComments.map((c) =>
+        commentToElkStep(c, stateStore, roundingFunction, layoutOptions)
+    );
+
+    return [...elkRootSteps, ...elkRootComments];
+}
+
+function stepToElkStep(
+    step: Step,
+    stateStore: ReturnType<typeof useWorkflowStateStore>,
+    roundingFunction: (value: number) => number
+): ElkNode {
+    const inputs = Object.values(step.inputs).map((input) => {
+        return {
+            id: `${step.id}/in/${input.name}`,
+            properties: {
+                "port.side": "WEST",
+                "port.index": step.id,
+            },
+        };
+    });
+
+    const outputs = Object.values(step.outputs).map((output) => {
+        return {
+            id: `${step.id}/out/${output.name}`,
+            properties: {
+                "port.side": "EAST",
+                "port.index": step.id,
+            },
+        };
+    });
+
+    const position = stateStore.stepPosition[step.id];
+    assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
+
+    return {
+        id: `${step.id}`,
+        height: roundingFunction(position.height),
+        width: roundingFunction(position.width),
+        ports: inputs.concat(outputs),
+    };
+}
+
+function commentToElkStep(
+    hierarchicalComment: HierarchicalComment,
+    stateStore: ReturnType<typeof useWorkflowStateStore>,
+    roundingFunction: (value: number) => number,
+    layoutOptions: Record<string, string>
+): ElkNode {
+    const base: ElkNode = {
+        id: `comment_${hierarchicalComment.comment.id}`,
+        x: hierarchicalComment.comment.position[0],
+        y: hierarchicalComment.comment.position[1],
+        width: hierarchicalComment.comment.size[0],
+        height: hierarchicalComment.comment.size[1],
+        layoutOptions: {
+            ...layoutOptions,
+            "elk.padding": elkSpacing(20, 40, 20, 20),
+        },
+    };
+
+    const children: ElkNode[] = hierarchicalComment.children?.map((c) => {
+        if ("comment" in c) {
+            return commentToElkStep(c, stateStore, roundingFunction, layoutOptions);
+        } else {
+            return stepToElkStep(c, stateStore, roundingFunction);
+        }
+    });
+
+    return { ...base, children };
+}
+
+interface Positions {
+    steps: { id: string; x: number; y: number }[];
+    comments: { id: string; x: number; y: number; w: number; h: number }[];
+}
+
+function graphToPositions(
+    graph: ElkNode[] | undefined,
+    roundingFunction: (value: number) => number,
+    parentPosition?: { x: number; y: number }
+): Positions {
+    const positions: Positions = {
+        steps: [],
+        comments: [],
+    };
+
+    if (!graph) {
+        return positions;
+    }
+
+    const offset = parentPosition ?? { x: 0, y: 0 };
+
+    graph.forEach((node) => {
+        if (!node.id.startsWith("comment_")) {
+            positions.steps.push({
+                id: node.id,
+                x: roundingFunction(node.x ?? 0) + offset.x,
+                y: roundingFunction(node.y ?? 0) + offset.y,
+            });
+        } else {
+            const id = node.id.slice("comment_".length);
+
+            const position = {
+                x: roundingFunction(node.x ?? 0) + offset.x,
+                y: roundingFunction(node.y ?? 0) + offset.y,
+            };
+
+            positions.comments.push({
+                id,
+                ...position,
+                w: node.width ?? 0,
+                h: node.height ?? 0,
+            });
+
+            if (node.children) {
+                const childPositions = graphToPositions(node.children, roundingFunction, position);
+
+                positions.steps = positions.steps.concat(childPositions.steps);
+                positions.comments = positions.comments.concat(childPositions.comments);
+            }
+        }
+    });
+
+    return positions;
+}

From ab1e8d9ca803b795783aeed37c4e4047545eb928 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 11 Nov 2024 22:19:40 +0100
Subject: [PATCH 070/131] undo comment auto layout

---
 .../src/components/Workflow/Editor/Actions/stepActions.ts | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts
index af424a07eb14..c58573cc59e0 100644
--- a/client/src/components/Workflow/Editor/Actions/stepActions.ts
+++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts
@@ -478,6 +478,14 @@ export class AutoLayoutAction extends UndoRedoAction {
             y: step.position?.top ?? 0,
         }));
 
+        this.oldPositions.comments = this.commentStore.comments.map((comment) => ({
+            id: `${comment.id}`,
+            x: comment.position[0],
+            y: comment.position[1],
+            w: comment.size[0],
+            h: comment.size[1],
+        }));
+
         const { autoLayout } = await import(
             /* webpackChunkName: "workflowLayout" */ "@/components/Workflow/Editor/modules/layout"
         );

From 45966ea3237a424cbe95aec6fe62b4801332e5e2 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 11 Nov 2024 22:26:15 +0100
Subject: [PATCH 071/131] consider comments in layout

---
 client/src/components/Workflow/Editor/modules/layout.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index e940111f8baf..5e37994498a1 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -202,6 +202,7 @@ function commentToElkStep(
         width: hierarchicalComment.comment.size[0],
         height: hierarchicalComment.comment.size[1],
         layoutOptions: {
+            "elk.commentBox": hierarchicalComment.comment.type === "frame" ? "false" : "true",
             ...layoutOptions,
             "elk.padding": elkSpacing(20, 40, 20, 20),
         },

From 628379ec2830c852501f7ec3b58c0cf24084b2d5 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 12 Nov 2024 00:22:55 +0100
Subject: [PATCH 072/131] move freehand comments with their closest comment
 preserving large drawings

---
 .../Workflow/Editor/modules/geometry.ts       |  22 +++
 .../Workflow/Editor/modules/layout.ts         | 181 +++++++++++++++++-
 2 files changed, 195 insertions(+), 8 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/geometry.ts b/client/src/components/Workflow/Editor/modules/geometry.ts
index eee7fc3d56be..03bea2c0a3dc 100644
--- a/client/src/components/Workflow/Editor/modules/geometry.ts
+++ b/client/src/components/Workflow/Editor/modules/geometry.ts
@@ -108,6 +108,15 @@ export class AxisAlignedBoundingBox implements Rectangle {
             this.endY >= other.y + other.height
         );
     }
+
+    intersects(other: Rectangle) {
+        return (
+            this.x < other.x + other.width &&
+            this.endX > other.x &&
+            this.y < other.y + other.height &&
+            this.endY > other.y
+        );
+    }
 }
 
 /* Format
@@ -240,3 +249,16 @@ export function vecReduceFigures(a: Vector, significantFigures = 1): Vector {
 
     return [Math.round(a[0] * factor) / factor, Math.round(a[1] * factor) / factor];
 }
+
+export function rectCenterPoint(rect: Rectangle): Vector {
+    return [rect.x + rect.width / 2, rect.y + rect.height / 2];
+}
+
+export function rectDistance(a: Rectangle, b: Rectangle): number {
+    const vecA = rectCenterPoint(a);
+    const vecB = rectCenterPoint(b);
+    const dx = vecA[0] - vecB[0];
+    const dy = vecA[1] - vecB[1];
+
+    return Math.hypot(dx, dy);
+}
diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 5e37994498a1..60add405bd06 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -1,12 +1,14 @@
 import ELK, { type ElkExtendedEdge, type ElkNode } from "elkjs/lib/elk.bundled";
 
 import { useConnectionStore } from "@/stores/workflowConnectionStore";
-import type { WorkflowComment } from "@/stores/workflowEditorCommentStore";
+import type { FreehandWorkflowComment, WorkflowComment } from "@/stores/workflowEditorCommentStore";
 import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
 import { type Step } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
 import { match } from "@/utils/utils";
 
+import { AxisAlignedBoundingBox, rectDistance } from "./geometry";
+
 const elk = new ELK();
 
 interface OptionObject {
@@ -55,7 +57,7 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
         return Math.ceil(value / snappingDistance - floatErrorTolerance) * snappingDistance;
     };
 
-    const baseLayoutOptions = {
+    const childLayoutOptions = {
         "elk.layered.spacing.nodeNodeBetweenLayers": `${horizontalDistance / 2}`,
     };
 
@@ -63,7 +65,6 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
     const newGraph: ElkNode = {
         id: "",
         layoutOptions: {
-            ...baseLayoutOptions,
             "elk.padding": elkSpacing(0, 0),
             "elk.hierarchyHandling": "INCLUDE_CHILDREN",
             "elk.layered.spacing.baseValue": `${horizontalDistance}`,
@@ -77,7 +78,27 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
         edges: [],
     };
 
-    newGraph.children = graphToElkGraph(steps, comments, stateStore, roundUpToSnappingDistance, baseLayoutOptions);
+    const freehandComments: FreehandWorkflowComment[] = [];
+    const otherComments: WorkflowComment[] = [];
+
+    comments.forEach((comment) => {
+        if (comment.type === "freehand") {
+            freehandComments.push(comment);
+        } else {
+            otherComments.push(comment);
+        }
+    });
+
+    const collapsedFreehandComments = collapseFreehandComments(freehandComments);
+    populateClosestSteps(collapsedFreehandComments, steps, stateStore);
+
+    newGraph.children = graphToElkGraph(
+        steps,
+        otherComments,
+        stateStore,
+        roundUpToSnappingDistance,
+        childLayoutOptions
+    );
 
     newGraph.edges = connectionStore.connections.map((connection) => {
         const edge: ElkExtendedEdge = {
@@ -93,7 +114,10 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
     try {
         const elkNode = await elk.layout(newGraph);
         const positions = graphToPositions(elkNode.children, roundToSnappingDistance);
-        console.log(positions);
+
+        const freehandPositions = resolveDeltaPositions(collapsedFreehandComments, positions.steps);
+        positions.comments = positions.comments.concat(freehandPositions);
+
         return positions;
     } catch (error) {
         console.error(error);
@@ -122,9 +146,12 @@ function graphToElkGraph(
     flatHierarchicalComments.forEach((c) => {
         if (c.comment.child_comments) {
             c.comment.child_comments.forEach((id) => {
-                const childComment = flatHierarchicalComments.get(id)!;
-                childComment.root = false;
-                c.children.push(childComment);
+                const childComment = flatHierarchicalComments.get(id);
+
+                if (childComment) {
+                    childComment.root = false;
+                    c.children.push(childComment);
+                }
             });
         }
 
@@ -273,3 +300,141 @@ function graphToPositions(
 
     return positions;
 }
+
+interface CollapsedFreehandComment {
+    aabb: AxisAlignedBoundingBox;
+    comments: FreehandWorkflowComment[];
+    closestStepId?: string;
+    positionFrom?: { x: number; y: number };
+}
+
+/** groups freehand comments into distinct sets with any amount of overlap */
+function collapseFreehandComments(comments: FreehandWorkflowComment[]): CollapsedFreehandComment[] {
+    const commentsAsCollapsed: CollapsedFreehandComment[] = comments.map((c) => {
+        const aabb = new AxisAlignedBoundingBox();
+        aabb.fitRectangle({
+            x: c.position[0],
+            y: c.position[1],
+            width: c.size[0],
+            height: c.size[1],
+        });
+
+        return {
+            aabb,
+            comments: [c],
+        };
+    });
+
+    const collapsedFreehandComments: Set<CollapsedFreehandComment> = new Set(commentsAsCollapsed);
+
+    const compareAgainstOtherCollapsed = (a: CollapsedFreehandComment) => {
+        const iterator = collapsedFreehandComments.values();
+
+        for (const other of iterator) {
+            if (a !== other && a.aabb.intersects(other.aabb)) {
+                mergeCollapsedComments(a, other);
+                break;
+            }
+        }
+    };
+
+    const mergeCollapsedComments = (a: CollapsedFreehandComment, b: CollapsedFreehandComment) => {
+        const aabb = new AxisAlignedBoundingBox();
+        aabb.fitRectangle(a.aabb);
+        aabb.fitRectangle(b.aabb);
+
+        collapsedFreehandComments.delete(a);
+        collapsedFreehandComments.delete(b);
+
+        const merged = {
+            aabb: aabb,
+            comments: [...a.comments, ...b.comments],
+        };
+
+        collapsedFreehandComments.add(merged);
+        compareAgainstOtherCollapsed(merged);
+    };
+
+    const iterator = collapsedFreehandComments.values();
+
+    for (const comment of iterator) {
+        compareAgainstOtherCollapsed(comment);
+    }
+
+    return [...collapsedFreehandComments.values()];
+}
+
+/** find out which step is the closest to each comment, save it's id and position */
+function populateClosestSteps(
+    collapsedFreehandComments: CollapsedFreehandComment[],
+    steps: Record<string, Step>,
+    stateStore: ReturnType<typeof useWorkflowStateStore>
+) {
+    const stepsWidthRect = Object.entries(steps).map(([stepId, step]) => {
+        const position = stateStore.stepPosition[step.id];
+        assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
+
+        return {
+            id: stepId,
+            step,
+            rect: {
+                x: step.position?.left ?? 0,
+                y: step.position?.top ?? 0,
+                width: position.width,
+                height: position.height,
+            },
+        };
+    });
+
+    collapsedFreehandComments.forEach((comment) => {
+        let closestDistance = Infinity;
+
+        stepsWidthRect.forEach((s) => {
+            const distance = rectDistance(comment.aabb, s.rect);
+            if (distance < closestDistance) {
+                closestDistance = distance;
+                comment.closestStepId = s.id;
+                comment.positionFrom = {
+                    x: s.rect.x,
+                    y: s.rect.y,
+                };
+            }
+        });
+    });
+}
+
+/** resolve by how much to move the freehand comments */
+function resolveDeltaPositions(
+    collapsedFreehandComments: CollapsedFreehandComment[],
+    stepPositions: Positions["steps"]
+): Positions["comments"] {
+    const positions: Positions["comments"] = [];
+    const stepPositionMap = new Map(stepPositions.map((p) => [p.id, p]));
+
+    collapsedFreehandComments.forEach((collapsed) => {
+        if (!collapsed.closestStepId) {
+            return;
+        }
+
+        const newPosition = stepPositionMap.get(collapsed.closestStepId);
+
+        if (newPosition) {
+            const delta = {
+                x: newPosition.x - (collapsed.positionFrom?.x ?? 0),
+                y: newPosition.y - (collapsed.positionFrom?.y ?? 0),
+            };
+
+            collapsed.comments.forEach((comment) => {
+                positions.push({
+                    id: `${comment.id}`,
+                    x: comment.position[0] + delta.x,
+                    y: comment.position[1] + delta.y,
+                    w: comment.size[0],
+                    h: comment.size[1],
+                });
+            });
+        }
+    });
+
+    return positions;
+}

From 4a9ecc58148c400bc7c539d383c28ecad1f35cc4 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 12 Nov 2024 00:49:09 +0100
Subject: [PATCH 073/131] fix edge crossing

---
 .../Workflow/Editor/modules/layout.ts         | 24 ++++++++++++-------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 60add405bd06..704b774ac26e 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -59,6 +59,7 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
 
     const childLayoutOptions = {
         "elk.layered.spacing.nodeNodeBetweenLayers": `${horizontalDistance / 2}`,
+        "elk.portConstraints": "FIXED_POS",
     };
 
     // Convert this to ELK compat.
@@ -71,8 +72,6 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
             "elk.algorithm": "layered",
             "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
             "elk.spacing.nodeNode": `${verticalDistance}`,
-            "crossingMinimization.semiInteractive": "true",
-            "elk.alignment": "TOP",
         },
         children: [],
         edges: [],
@@ -185,33 +184,40 @@ function stepToElkStep(
     stateStore: ReturnType<typeof useWorkflowStateStore>,
     roundingFunction: (value: number) => number
 ): ElkNode {
-    const inputs = Object.values(step.inputs).map((input) => {
+    const inputs = Object.values(step.inputs).map((input, index) => {
         return {
             id: `${step.id}/in/${input.name}`,
             properties: {
                 "port.side": "WEST",
-                "port.index": step.id,
+                "port.index": `${index}`,
             },
+            x: 0,
+            y: index * 20,
         };
     });
 
-    const outputs = Object.values(step.outputs).map((output) => {
+    const position = stateStore.stepPosition[step.id];
+    assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
+
+    const outputs = Object.values(step.outputs).map((output, index) => {
         return {
             id: `${step.id}/out/${output.name}`,
             properties: {
                 "port.side": "EAST",
-                "port.index": step.id,
+                "port.index": `${index}`,
             },
+            x: position.width,
+            y: index * 20,
         };
     });
 
-    const position = stateStore.stepPosition[step.id];
-    assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
-
     return {
         id: `${step.id}`,
         height: roundingFunction(position.height),
         width: roundingFunction(position.width),
+        layoutOptions: {
+            "elk.portConstraints": "FIXED_POS",
+        },
         ports: inputs.concat(outputs),
     };
 }

From 1e3a68a2aea67ce2c67d8d885296140c4796aff6 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 12 Nov 2024 01:01:59 +0100
Subject: [PATCH 074/131] associate comments to their closest node

---
 .../Workflow/Editor/modules/layout.ts         | 96 ++++++++++++++-----
 1 file changed, 71 insertions(+), 25 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 704b774ac26e..3c6de54844cd 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -7,7 +7,7 @@ import { type Step } from "@/stores/workflowStepStore";
 import { assertDefined } from "@/utils/assertions";
 import { match } from "@/utils/utils";
 
-import { AxisAlignedBoundingBox, rectDistance } from "./geometry";
+import { AxisAlignedBoundingBox, type Rectangle, rectDistance } from "./geometry";
 
 const elk = new ELK();
 
@@ -88,8 +88,24 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
         }
     });
 
+    const stepsWithRect = Object.entries(steps).map(([stepId, step]) => {
+        const position = stateStore.stepPosition[step.id];
+        assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
+
+        return {
+            id: stepId,
+            step,
+            rect: {
+                x: step.position?.left ?? 0,
+                y: step.position?.top ?? 0,
+                width: position.width,
+                height: position.height,
+            },
+        };
+    });
+
     const collapsedFreehandComments = collapseFreehandComments(freehandComments);
-    populateClosestSteps(collapsedFreehandComments, steps, stateStore);
+    populateClosestSteps(collapsedFreehandComments, stepsWithRect);
 
     newGraph.children = graphToElkGraph(
         steps,
@@ -99,7 +115,7 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
         childLayoutOptions
     );
 
-    newGraph.edges = connectionStore.connections.map((connection) => {
+    const dataEdges = connectionStore.connections.map((connection) => {
         const edge: ElkExtendedEdge = {
             id: `e_${connection.input.stepId}_${connection.output.stepId}`,
             sources: [`${connection.output.stepId}/out/${connection.output.name}`],
@@ -108,6 +124,10 @@ export async function autoLayout(id: string, steps: { [index: string]: Step }, c
         return edge;
     });
 
+    const commentEdges = getCommentEdges(otherComments, stepsWithRect);
+
+    newGraph.edges = [...dataEdges, ...commentEdges];
+
     const roundToSnappingDistance = (value: number) => Math.round(value / snappingDistance) * snappingDistance;
 
     try {
@@ -307,6 +327,46 @@ function graphToPositions(
     return positions;
 }
 
+function getCommentEdges(comments: WorkflowComment[], stepsWithRect: StepWithRect[]): ElkExtendedEdge[] {
+    const edges: ElkExtendedEdge[] = [];
+
+    comments.forEach((comment) => {
+        if (comment.type === "freehand") {
+            return;
+        }
+
+        let closestDistance = Infinity;
+        let closestId: string | null = null;
+
+        const commentRect = {
+            x: comment.position[0],
+            y: comment.position[1],
+            width: comment.size[0],
+            height: comment.size[1],
+        };
+
+        stepsWithRect.forEach((step) => {
+            const distance = rectDistance(step.rect, commentRect);
+            if (distance < closestDistance) {
+                closestDistance = distance;
+                closestId = step.id;
+            }
+        });
+
+        if (closestId) {
+            const edge: ElkExtendedEdge = {
+                id: `comment_edge_${closestId}_${comment.id}`,
+                sources: [closestId],
+                targets: [`comment_${comment.id}`],
+            };
+
+            edges.push(edge);
+        }
+    });
+
+    return edges;
+}
+
 interface CollapsedFreehandComment {
     aabb: AxisAlignedBoundingBox;
     comments: FreehandWorkflowComment[];
@@ -370,32 +430,18 @@ function collapseFreehandComments(comments: FreehandWorkflowComment[]): Collapse
     return [...collapsedFreehandComments.values()];
 }
 
-/** find out which step is the closest to each comment, save it's id and position */
-function populateClosestSteps(
-    collapsedFreehandComments: CollapsedFreehandComment[],
-    steps: Record<string, Step>,
-    stateStore: ReturnType<typeof useWorkflowStateStore>
-) {
-    const stepsWidthRect = Object.entries(steps).map(([stepId, step]) => {
-        const position = stateStore.stepPosition[step.id];
-        assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
-
-        return {
-            id: stepId,
-            step,
-            rect: {
-                x: step.position?.left ?? 0,
-                y: step.position?.top ?? 0,
-                width: position.width,
-                height: position.height,
-            },
-        };
-    });
+interface StepWithRect {
+    id: string;
+    step: Step;
+    rect: Rectangle;
+}
 
+/** find out which step is the closest to each comment, save it's id and position */
+function populateClosestSteps(collapsedFreehandComments: CollapsedFreehandComment[], stepsWithRect: StepWithRect[]) {
     collapsedFreehandComments.forEach((comment) => {
         let closestDistance = Infinity;
 
-        stepsWidthRect.forEach((s) => {
+        stepsWithRect.forEach((s) => {
             const distance = rectDistance(comment.aabb, s.rect);
             if (distance < closestDistance) {
                 closestDistance = distance;

From af0d8faf4e275edcf3bc4dac0fb16429c00dc809 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 12 Nov 2024 01:14:20 +0100
Subject: [PATCH 075/131] make auto layout a toolbar tool

---
 .../src/components/Workflow/Editor/Index.vue  | 10 +--------
 .../Workflow/Editor/Tools/ToolBar.vue         | 22 ++++++++++++++++---
 .../Workflow/Editor/modules/activities.ts     | 11 ----------
 client/src/composables/workflowStores.ts      |  1 +
 4 files changed, 21 insertions(+), 23 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 6038631d2aed..fcd960f5c7d9 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -202,7 +202,7 @@ import { LastQueue } from "@/utils/lastQueue";
 import { errorMessageAsString } from "@/utils/simple-error";
 
 import { Services } from "../services";
-import { AutoLayoutAction, InsertStepAction, useStepActions } from "./Actions/stepActions";
+import { InsertStepAction, useStepActions } from "./Actions/stepActions";
 import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
 import { defaultPosition } from "./composables/useDefaultStepPosition";
 import { useSpecialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
@@ -744,10 +744,6 @@ export default {
                 this.onUpgrade();
             }
 
-            if (activityId === "workflow-auto-layout") {
-                this.onLayout();
-            }
-
             if (activityId === "workflow-run") {
                 this.onRun();
             }
@@ -756,10 +752,6 @@ export default {
                 await this.saveOrCreate();
             }
         },
-        onLayout() {
-            const action = new AutoLayoutAction(this.id);
-            this.undoRedoStore.applyAction(action);
-        },
         onAnnotation(nodeId, newAnnotation) {
             this.stepActions.setAnnotation(this.steps[nodeId], newAnnotation);
         },
diff --git a/client/src/components/Workflow/Editor/Tools/ToolBar.vue b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
index c902f381bccf..0f2c38d7b086 100644
--- a/client/src/components/Workflow/Editor/Tools/ToolBar.vue
+++ b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
@@ -17,7 +17,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { useMagicKeys, whenever } from "@vueuse/core";
 import { BButton, BButtonGroup, BFormInput } from "bootstrap-vue";
 //@ts-ignore deprecated package without types (vue 2, remove this comment on vue 3 migration)
-import { BoxSelect } from "lucide-vue";
+import { BoxSelect, Workflow } from "lucide-vue";
 import { storeToRefs } from "pinia";
 import { computed, toRefs, watch } from "vue";
 
@@ -27,6 +27,7 @@ import { useWorkflowStores } from "@/composables/workflowStores";
 import { type CommentTool } from "@/stores/workflowEditorToolbarStore";
 import { match } from "@/utils/utils";
 
+import { AutoLayoutAction } from "../Actions/stepActions";
 import { useSelectionOperations } from "./useSelectionOperations";
 import { useToolLogic } from "./useToolLogic";
 
@@ -46,7 +47,7 @@ library.add(
     faTrash
 );
 
-const { toolbarStore, undoRedoStore, commentStore } = useWorkflowStores();
+const { toolbarStore, undoRedoStore, commentStore, workflowId } = useWorkflowStores();
 const { snapActive, currentTool } = toRefs(toolbarStore);
 
 const { commentOptions } = toolbarStore;
@@ -128,7 +129,7 @@ function onRemoveAllFreehand() {
 
 useToolLogic();
 
-const { ctrl_1, ctrl_2, ctrl_3, ctrl_4, ctrl_5, ctrl_6, ctrl_7, ctrl_8 } = useMagicKeys();
+const { ctrl_1, ctrl_2, ctrl_3, ctrl_4, ctrl_5, ctrl_6, ctrl_7, ctrl_8, ctrl_9 } = useMagicKeys();
 
 whenever(ctrl_1!, () => (toolbarStore.currentTool = "pointer"));
 whenever(ctrl_2!, () => (toolbarStore.snapActive = !toolbarStore.snapActive));
@@ -138,6 +139,7 @@ whenever(ctrl_5!, () => (toolbarStore.currentTool = "frameComment"));
 whenever(ctrl_6!, () => (toolbarStore.currentTool = "freehandComment"));
 whenever(ctrl_7!, () => (toolbarStore.currentTool = "freehandEraser"));
 whenever(ctrl_8!, () => (toolbarStore.currentTool = "boxSelect"));
+whenever(ctrl_9!, () => autoLayout());
 
 const toggleVisibilityButtonTitle = computed(() => {
     if (toolbarVisible.value) {
@@ -148,6 +150,10 @@ const toggleVisibilityButtonTitle = computed(() => {
 });
 
 const { anySelected, selectedCountText, deleteSelection, deselectAll, duplicateSelection } = useSelectionOperations();
+
+function autoLayout() {
+    undoRedoStore.applyAction(new AutoLayoutAction(workflowId));
+}
 </script>
 
 <template>
@@ -242,6 +248,16 @@ const { anySelected, selectedCountText, deleteSelection, deselectAll, duplicateS
                     @click="onClickBoxSelect">
                     <BoxSelect />
                 </BButton>
+
+                <BButton
+                    v-b-tooltip.hover.noninteractive.right
+                    title="Auto Layout (Ctrl + 9)"
+                    data-tool="auto_layout"
+                    class="button"
+                    variant="outline-primary"
+                    @click="autoLayout">
+                    <Workflow />
+                </BButton>
             </template>
 
             <BButton
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index 10729d6aaa9e..eb5fd4efd804 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -1,6 +1,5 @@
 import { faSave as farSave } from "@fortawesome/free-regular-svg-icons";
 import {
-    faAlignLeft,
     faDownload,
     faEdit,
     faHistory,
@@ -96,16 +95,6 @@ export const workflowEditorActivities = [
         click: true,
         optional: true,
     },
-    {
-        title: "Auto Layout",
-        id: "workflow-auto-layout",
-        description: "Automatically align the nodes in this workflow.",
-        tooltip: "Automatically align nodes",
-        icon: faAlignLeft,
-        visible: true,
-        click: true,
-        optional: true,
-    },
     {
         title: "Upgrade",
         id: "workflow-upgrade",
diff --git a/client/src/composables/workflowStores.ts b/client/src/composables/workflowStores.ts
index 79338bacfffb..63704068e3c9 100644
--- a/client/src/composables/workflowStores.ts
+++ b/client/src/composables/workflowStores.ts
@@ -76,6 +76,7 @@ export function useWorkflowStores(workflowId?: Ref<string> | string) {
     const undoRedoStore = useUndoRedoStore(id);
 
     return {
+        workflowId: id,
         connectionStore,
         stateStore,
         stepStore,

From 797bbb4da2439c31c0889a33c9e0072ad70c1029 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 12 Nov 2024 01:16:25 +0100
Subject: [PATCH 076/131] pass step position for more accurate comment
 positioning

---
 client/src/components/Workflow/Editor/modules/layout.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts
index 3c6de54844cd..843867c7def1 100644
--- a/client/src/components/Workflow/Editor/modules/layout.ts
+++ b/client/src/components/Workflow/Editor/modules/layout.ts
@@ -235,6 +235,8 @@ function stepToElkStep(
         id: `${step.id}`,
         height: roundingFunction(position.height),
         width: roundingFunction(position.width),
+        x: step.position?.left,
+        y: step.position?.top,
         layoutOptions: {
             "elk.portConstraints": "FIXED_POS",
         },

From 9fbfdc8a3c4751a026cf051c969d408fd6088cfd Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 13 Nov 2024 23:42:21 +0100
Subject: [PATCH 077/131] disable run button on new workflow

---
 .../components/ActivityBar/ActivityBar.vue    | 18 ++++++---
 .../components/ActivityBar/ActivityItem.vue   | 10 ++++-
 .../src/components/Workflow/Editor/Index.vue  | 11 ++++--
 .../Workflow/Editor/modules/activities.ts     | 21 ++++++++--
 client/src/stores/activityStore.ts            | 39 ++++++++++++++++++-
 5 files changed, 84 insertions(+), 15 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index d026d531a1f8..cf9096bd9fbb 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -237,8 +237,9 @@ defineExpose({
                                 @click="toggleSidebar()" />
                             <ActivityItem
                                 v-else-if="activity.id === 'admin' || activity.panel"
-                                :id="`activity-${activity.id}`"
+                                :id="`${activity.id}`"
                                 :key="activity.id"
+                                :activity-bar-id="props.activityBarId"
                                 :icon="activity.icon"
                                 :is-active="panelActivityIsActive(activity)"
                                 :title="activity.title"
@@ -247,8 +248,9 @@ defineExpose({
                                 @click="toggleSidebar(activity.id, activity.to)" />
                             <ActivityItem
                                 v-else
-                                :id="`activity-${activity.id}`"
+                                :id="`${activity.id}`"
                                 :key="activity.id"
+                                :activity-bar-id="props.activityBarId"
                                 :icon="activity.icon"
                                 :is-active="isActiveRoute(activity.to)"
                                 :title="activity.title"
@@ -269,7 +271,8 @@ defineExpose({
                     title="Notifications"
                     @click="toggleSidebar('notifications')" />
                 <ActivityItem
-                    id="activity-settings"
+                    id="settings"
+                    :activity-bar-id="props.activityBarId"
                     :icon="props.optionsIcon"
                     :is-active="isActiveSideBar('settings')"
                     :title="props.optionsTitle"
@@ -277,7 +280,8 @@ defineExpose({
                     @click="toggleSidebar('settings')" />
                 <ActivityItem
                     v-if="isAdmin && showAdmin"
-                    id="activity-admin"
+                    id="admin"
+                    :activity-bar-id="props.activityBarId"
                     icon="user-cog"
                     :is-active="isActiveSideBar('admin')"
                     title="Admin"
@@ -287,8 +291,9 @@ defineExpose({
                 <template v-for="activity in props.specialActivities">
                     <ActivityItem
                         v-if="activity.panel"
-                        :id="`activity-${activity.id}`"
+                        :id="`${activity.id}`"
                         :key="activity.id"
+                        :activity-bar-id="props.activityBarId"
                         :icon="activity.icon"
                         :is-active="panelActivityIsActive(activity)"
                         :title="activity.title"
@@ -298,8 +303,9 @@ defineExpose({
                         @click="toggleSidebar(activity.id, activity.to)" />
                     <ActivityItem
                         v-else
-                        :id="`activity-${activity.id}`"
+                        :id="`${activity.id}`"
                         :key="activity.id"
+                        :activity-bar-id="props.activityBarId"
                         :icon="activity.icon"
                         :is-active="isActiveRoute(activity.to)"
                         :title="activity.title"
diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index e715742a15b1..279d91d10edc 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import type { Placement } from "@popperjs/core";
+import { computed } from "vue";
 import { useRouter } from "vue-router/composables";
 
-import type { ActivityVariant } from "@/stores/activityStore";
+import { type ActivityVariant, useActivityStore } from "@/stores/activityStore";
 import localize from "@/utils/localization";
 
 import TextShort from "@/components/Common/TextShort.vue";
@@ -18,6 +19,7 @@ interface Option {
 
 export interface Props {
     id: string;
+    activityBarId: string;
     title?: string;
     icon?: string | object;
     indicator?: number;
@@ -55,17 +57,21 @@ function onClick(evt: MouseEvent): void {
         router.push(props.to);
     }
 }
+
+const store = useActivityStore(props.activityBarId);
+const meta = computed(() => store.metaForId(props.id));
 </script>
 
 <template>
     <Popper reference-is="span" popper-is="span" :placement="tooltipPlacement">
         <template v-slot:reference>
             <b-nav-item
-                :id="id"
+                :id="`activity-${id}`"
                 class="activity-item position-relative my-1 p-2"
                 :class="{ 'nav-item-active': isActive }"
                 :link-classes="`variant-${props.variant}`"
                 :aria-label="localize(title)"
+                :disabled="meta?.disabled"
                 @click="onClick">
                 <span v-if="progressStatus" class="progress">
                     <div
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index fcd960f5c7d9..085a7cc7ce99 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -205,7 +205,7 @@ import { Services } from "../services";
 import { InsertStepAction, useStepActions } from "./Actions/stepActions";
 import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
 import { defaultPosition } from "./composables/useDefaultStepPosition";
-import { useSpecialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
+import { useActivityLogic, useSpecialWorkflowActivities, workflowEditorActivities } from "./modules/activities";
 import { fromSimple } from "./modules/model";
 import { getModule, getVersions, loadWorkflow, saveWorkflow } from "./modules/services";
 import { getStateUpgradeMessages } from "./modules/utilities";
@@ -449,9 +449,7 @@ export default {
 
         const { specialWorkflowActivities } = useSpecialWorkflowActivities(
             computed(() => ({
-                hasChanges: hasChanges.value,
                 hasInvalidConnections: hasInvalidConnections.value,
-                isNewTempWorkflow: isNewTempWorkflow.value,
             }))
         );
 
@@ -461,6 +459,13 @@ export default {
                 : "Save Workflow"
         );
 
+        useActivityLogic(
+            computed(() => ({
+                activityBarId: "workflow-editor",
+                isNewTempWorkflow: isNewTempWorkflow.value,
+            }))
+        );
+
         return {
             id,
             name,
diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index eb5fd4efd804..f85c012b2a48 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -12,9 +12,10 @@ import {
     faSitemap,
     faWrench,
 } from "@fortawesome/free-solid-svg-icons";
+import { watchImmediate } from "@vueuse/core";
 import { computed, type Ref } from "vue";
 
-import type { Activity } from "@/stores/activityStore";
+import { type Activity, useActivityStore } from "@/stores/activityStore";
 
 export const workflowEditorActivities = [
     {
@@ -127,9 +128,23 @@ export const workflowEditorActivities = [
     },
 ] as const satisfies Readonly<Activity[]>;
 
-interface SpecialActivityOptions {
+interface ActivityLogicOptions {
+    activityBarId: string;
     isNewTempWorkflow: boolean;
-    hasChanges: boolean;
+}
+
+export function useActivityLogic(options: Ref<ActivityLogicOptions>) {
+    const store = useActivityStore(options.value.activityBarId);
+
+    watchImmediate(
+        () => options.value.isNewTempWorkflow,
+        (value) => {
+            store.setMeta("workflow-run", "disabled", value);
+        }
+    );
+}
+
+interface SpecialActivityOptions {
     hasInvalidConnections: boolean;
 }
 
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index f6f4006a5577..341b1a0e45dc 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -2,10 +2,11 @@
  * Stores the Activity Bar state
  */
 import { useDebounceFn, watchImmediate } from "@vueuse/core";
-import { computed, type Ref, ref } from "vue";
+import { computed, type Ref, ref, set } from "vue";
 
 import { useHashedUserId } from "@/composables/hashedUserId";
 import { useUserLocalStorage } from "@/composables/userLocalStorage";
+import { ensureDefined } from "@/utils/assertions";
 
 import { defaultActivities } from "./activitySetup";
 import { defineScopedStore } from "./scopedStore";
@@ -40,8 +41,19 @@ export interface Activity {
     variant?: ActivityVariant;
 }
 
+export interface ActivityMeta {
+    disabled: boolean;
+}
+
+function defaultActivityMeta(): ActivityMeta {
+    return {
+        disabled: false,
+    };
+}
+
 export const useActivityStore = defineScopedStore("activityStore", (scope) => {
     const activities: Ref<Array<Activity>> = useUserLocalStorage(`activity-store-activities-${scope}`, []);
+    const activityMeta: Ref<Record<string, ActivityMeta>> = ref({});
 
     const { hashedUserId } = useHashedUserId();
 
@@ -154,6 +166,28 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         }
     }
 
+    const metaForId = computed(() => (activityId: string) => {
+        let meta = activityMeta.value[activityId];
+
+        if (!meta) {
+            set(activityMeta.value, activityId, defaultActivityMeta());
+            meta = ensureDefined(activityMeta.value[activityId]);
+        }
+
+        return meta;
+    });
+
+    function setMeta<K extends keyof ActivityMeta>(activityId: string, metaKey: K, value: ActivityMeta[K]) {
+        let meta = activityMeta.value[activityId];
+
+        if (!meta) {
+            set(activityMeta.value, activityId, defaultActivityMeta());
+            meta = ensureDefined(activityMeta.value[activityId]);
+        }
+
+        set(meta, metaKey, value);
+    }
+
     watchImmediate(
         () => hashedUserId.value,
         () => {
@@ -165,6 +199,9 @@ export const useActivityStore = defineScopedStore("activityStore", (scope) => {
         toggledSideBar,
         toggleSideBar,
         activities,
+        activityMeta,
+        metaForId,
+        setMeta,
         getAll,
         remove,
         setAll,

From 23ac65939e668a571241ed15b0b70807705cbc00 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 14:57:31 +0100
Subject: [PATCH 078/131] Update
 client/src/components/ActivityBar/ActivityBar.vue

Remove redundant checks
---
 client/src/components/ActivityBar/ActivityBar.vue | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index cf9096bd9fbb..169b90eed2c5 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -115,10 +115,7 @@ const isSideBarOpen = computed(() => activityStore.toggledSideBar !== "");
  * Checks if an activity that has a panel should have the `is-active` prop
  */
 function panelActivityIsActive(activity: Activity) {
-    return (
-        isActiveSideBar(activity.id) ||
-        (activity.to !== undefined && activity.to !== null && isActiveRoute(activity.to))
-    );
+    return isActiveSideBar(activity.id) || isActiveRoute(activity.to);
 }
 
 /**

From 3d1761a32f180c65cc9ac8aa3a4c14354b4fafd5 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 14:51:58 +0100
Subject: [PATCH 079/131] Updates bookmark toggle function parameters

Fixing bookmarking/unbookmarking workflows. Modifies the `toggleBookmark` function calls to include boolean arguments indicating the bookmark state.
---
 client/src/components/Workflow/List/WorkflowActions.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue
index 3bcdc2427fb7..fbfea4769320 100644
--- a/client/src/components/Workflow/List/WorkflowActions.vue
+++ b/client/src/components/Workflow/List/WorkflowActions.vue
@@ -82,7 +82,7 @@ const runPath = computed(
                 title="Add to bookmarks"
                 tooltip="Add to bookmarks. This workflow will appear in the left tool panel."
                 size="sm"
-                @click="toggleBookmark">
+                @click="toggleBookmark(true)">
                 <FontAwesomeIcon v-if="!bookmarkLoading" :icon="farStar" fixed-width />
                 <FontAwesomeIcon v-else :icon="faSpinner" spin fixed-width />
             </BButton>
@@ -93,7 +93,7 @@ const runPath = computed(
                 variant="link"
                 title="Remove bookmark"
                 size="sm"
-                @click="toggleBookmark">
+                @click="toggleBookmark(false)">
                 <FontAwesomeIcon v-if="!bookmarkLoading" :icon="faStar" fixed-width />
                 <FontAwesomeIcon v-else :icon="faSpinner" spin fixed-width />
             </BButton>

From efddffb719bc546219271703310fc96ab6fcbf05 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 14:55:47 +0100
Subject: [PATCH 080/131] Removes non-existent config property usage

Eliminates references to a non-existent config property *stored_workflow_menu_entries*  in useWorkflowActions to prevent errors and clean up the code. Client does not have access to this property anymore and is not used anywhere
---
 .../components/Workflow/List/useWorkflowActions.ts | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/client/src/components/Workflow/List/useWorkflowActions.ts b/client/src/components/Workflow/List/useWorkflowActions.ts
index 8b0855fb4bc0..26a152699913 100644
--- a/client/src/components/Workflow/List/useWorkflowActions.ts
+++ b/client/src/components/Workflow/List/useWorkflowActions.ts
@@ -5,7 +5,6 @@ import {
     deleteWorkflow as deleteWorkflowService,
     updateWorkflow as updateWorkflowService,
 } from "@/components/Workflow/workflows.services";
-import { useConfig } from "@/composables/config";
 import { useConfirmDialog } from "@/composables/confirmDialog";
 import { useToast } from "@/composables/toast";
 import { copy } from "@/utils/clipboard";
@@ -17,7 +16,6 @@ type Workflow = any;
 
 export function useWorkflowActions(workflow: Ref<Workflow>, refreshCallback: () => void) {
     const toast = useToast();
-    const { config } = useConfig() as { config: Record<string, any> };
 
     const bookmarkLoading = ref(false);
 
@@ -30,18 +28,6 @@ export function useWorkflowActions(workflow: Ref<Workflow>, refreshCallback: ()
             });
 
             toast.info(`Workflow ${checked ? "added to" : "removed from"} bookmarks`);
-
-            if (checked) {
-                config.stored_workflow_menu_entries.push({
-                    id: workflow.value.id,
-                    name: workflow.value.name,
-                });
-            } else {
-                const indexToRemove = config.stored_workflow_menu_entries.findIndex(
-                    (w: Workflow) => w.id === workflow.value.id
-                );
-                config.stored_workflow_menu_entries.splice(indexToRemove, 1);
-            }
         } catch (error) {
             toast.error("Failed to update workflow bookmark status");
         } finally {

From 30deee4df277865d82608b6fa1b1142d8e472477 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 15:12:53 +0100
Subject: [PATCH 081/131] Adds workflowId to useStores return object

Includes workflowId in the return object of useStores function in terminals unit test. Fixing test error.
---
 client/src/components/Workflow/Editor/modules/terminals.test.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/client/src/components/Workflow/Editor/modules/terminals.test.ts b/client/src/components/Workflow/Editor/modules/terminals.test.ts
index f72e83896ab7..b90fb55cc119 100644
--- a/client/src/components/Workflow/Editor/modules/terminals.test.ts
+++ b/client/src/components/Workflow/Editor/modules/terminals.test.ts
@@ -41,6 +41,7 @@ function useStores(id = "mock-workflow") {
     const undoRedoStore = useUndoRedoStore(id);
 
     return {
+        workflowId: id,
         connectionStore,
         stateStore,
         stepStore,

From 882237a03953974d89e7296a57f5add5c09d7f54 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 17:45:08 +0100
Subject: [PATCH 082/131] Adds computed property for tools favorites toggle

Introduces a computed property to manage the favorites toggle state
Replaces the onFavorites function with a watcher for query changes
Simplifies the FavoritesButton binding using v-model.
---
 client/src/components/Panels/ToolPanel.vue | 28 ++++++++++++++++------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue
index e69f51d37591..87568f488c1f 100644
--- a/client/src/components/Panels/ToolPanel.vue
+++ b/client/src/components/Panels/ToolPanel.vue
@@ -100,6 +100,21 @@ const viewIcon = computed(() => {
     }
 });
 
+const showFavorites = computed({
+    get() {
+        return query.value.includes("#favorites");
+    },
+    set(value) {
+        if (value) {
+            if (!query.value.includes("#favorites")) {
+                query.value = `#favorites ${query.value}`.trim();
+            }
+        } else {
+            query.value = query.value.replace("#favorites", "").trim();
+        }
+    },
+});
+
 async function initializeTools() {
     try {
         await toolStore.fetchTools();
@@ -131,13 +146,12 @@ function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | u
     emit("onInsertWorkflowSteps", workflowId, workflowStepCount);
 }
 
-function onFavorites(favorites: boolean) {
-    if (favorites) {
-        query.value = "#favorites";
-    } else {
-        query.value = "";
+watch(
+    () => query.value,
+    (newQuery) => {
+        showFavorites.value = newQuery.includes("#favorites");
     }
-}
+);
 </script>
 
 <template>
@@ -177,7 +191,7 @@ function onFavorites(favorites: boolean) {
                     </template>
                 </PanelViewMenu>
                 <div v-if="!showAdvanced" class="panel-header-buttons">
-                    <FavoritesButton :query="query" @toggleFavorites="onFavorites" />
+                    <FavoritesButton v-model="showFavorites" />
                 </div>
             </div>
         </div>

From a64c2214ab943bf21d97cd4a0efccd6936e2aea6 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 17:46:30 +0100
Subject: [PATCH 083/131] Fixes FontAwesome icon usage in ToolPanel

Removes unnecessary library import and addition
Updates FontAwesomeIcon component to use dynamic icon binding
---
 client/src/components/Panels/ToolPanel.vue | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue
index 87568f488c1f..01b9c9028cf3 100644
--- a/client/src/components/Panels/ToolPanel.vue
+++ b/client/src/components/Panels/ToolPanel.vue
@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
 import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { storeToRefs } from "pinia";
@@ -16,8 +15,6 @@ import PanelViewMenu from "./Menus/PanelViewMenu.vue";
 import ToolBox from "./ToolBox.vue";
 import Heading from "@/components/Common/Heading.vue";
 
-library.add(faCaretDown);
-
 const props = defineProps({
     workflow: { type: Boolean, default: false },
     editorWorkflows: { type: Array, default: null },
@@ -185,7 +182,7 @@ watch(
                                 </Heading>
                             </div>
                             <div v-if="!showAdvanced" class="panel-header-buttons">
-                                <FontAwesomeIcon icon="caret-down" />
+                                <FontAwesomeIcon :icon="faCaretDown" />
                             </div>
                         </div>
                     </template>

From 3974dc5c706b4b2fe9a5a638b8e34c6057a1c0ad Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 18:07:05 +0100
Subject: [PATCH 084/131] Fix the test to check the shared indicator tooltip
 text

---
 client/src/components/Workflow/WorkflowAnnotation.test.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/client/src/components/Workflow/WorkflowAnnotation.test.ts b/client/src/components/Workflow/WorkflowAnnotation.test.ts
index abb7a4f5c062..8ebc19904b64 100644
--- a/client/src/components/Workflow/WorkflowAnnotation.test.ts
+++ b/client/src/components/Workflow/WorkflowAnnotation.test.ts
@@ -142,9 +142,7 @@ describe("WorkflowAnnotation renders", () => {
 
             const indicatorsLink = wrapper.find(SELECTORS.INDICATORS_LINK);
             expect(indicatorsLink.text()).toBe(WORKFLOW_OWNER);
-            expect(indicatorsLink.attributes("title")).toContain(
-                `Click to view all published workflows by '${WORKFLOW_OWNER}'`
-            );
+            expect(indicatorsLink.attributes("title")).toContain(`Published by '${WORKFLOW_OWNER}'`);
         }
         await checkHasIndicators("run_form");
         await checkHasIndicators("invocation");

From ff090af7b4e91d6c240d8e8b652c138590392244 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 18:12:04 +0100
Subject: [PATCH 085/131] Adds Pinia testing support to ActivityItem tests and
 add required prop activityBarId

---
 client/src/components/ActivityBar/ActivityItem.test.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/client/src/components/ActivityBar/ActivityItem.test.js b/client/src/components/ActivityBar/ActivityItem.test.js
index df919558fbae..c1177158befa 100644
--- a/client/src/components/ActivityBar/ActivityItem.test.js
+++ b/client/src/components/ActivityBar/ActivityItem.test.js
@@ -1,3 +1,4 @@
+import { createTestingPinia } from "@pinia/testing";
 import { mount } from "@vue/test-utils";
 import { getLocalVue } from "tests/jest/helpers";
 
@@ -12,6 +13,7 @@ describe("ActivityItem", () => {
         wrapper = mount(mountTarget, {
             propsData: {
                 id: "activity-test-id",
+                activityBarId: "activity-bar-test-id",
                 icon: "activity-test-icon",
                 indicator: 0,
                 progressPercentage: 0,
@@ -20,6 +22,7 @@ describe("ActivityItem", () => {
                 to: null,
                 tooltip: "activity-test-tooltip",
             },
+            pinia: createTestingPinia(),
             localVue,
             stubs: {
                 FontAwesomeIcon: true,

From 12bb3761612a266ace1f5e72acccd3dc659a0187 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 18:40:53 +0100
Subject: [PATCH 086/131] Updates workflow type to StoredWorkflowDetailed

Replaces 'any' type with 'StoredWorkflowDetailed' for workflow props
Improves type safety and code readability in Workflow components
---
 client/src/components/Workflow/List/WorkflowActions.vue       | 3 ++-
 client/src/components/Workflow/List/WorkflowActionsExtend.vue | 3 ++-
 client/src/components/Workflow/List/useWorkflowActions.ts     | 4 ++--
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue
index fbfea4769320..7202b22756d0 100644
--- a/client/src/components/Workflow/List/WorkflowActions.vue
+++ b/client/src/components/Workflow/List/WorkflowActions.vue
@@ -18,12 +18,13 @@ import { BButton, BButtonGroup, BDropdown, BDropdownItem } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
+import type { StoredWorkflowDetailed } from "@/api/workflows";
 import { useUserStore } from "@/stores/userStore";
 
 import { useWorkflowActions } from "./useWorkflowActions";
 
 interface Props {
-    workflow: any;
+    workflow: StoredWorkflowDetailed;
     published?: boolean;
     editor?: boolean;
     current?: boolean;
diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
index 6da91c1a8a66..56082d79ac9c 100644
--- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue
+++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue
@@ -14,6 +14,7 @@ import { BButton, BButtonGroup } from "bootstrap-vue";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
+import type { StoredWorkflowDetailed } from "@/api/workflows";
 import { undeleteWorkflow } from "@/components/Workflow/workflows.services";
 import { useConfirmDialog } from "@/composables/confirmDialog";
 import { Toast } from "@/composables/toast";
@@ -25,7 +26,7 @@ import AsyncButton from "@/components/Common/AsyncButton.vue";
 import WorkflowRunButton from "@/components/Workflow/WorkflowRunButton.vue";
 
 interface Props {
-    workflow: any;
+    workflow: StoredWorkflowDetailed;
     published?: boolean;
     editor?: boolean;
     current?: boolean;
diff --git a/client/src/components/Workflow/List/useWorkflowActions.ts b/client/src/components/Workflow/List/useWorkflowActions.ts
index 26a152699913..d1900f86a5a6 100644
--- a/client/src/components/Workflow/List/useWorkflowActions.ts
+++ b/client/src/components/Workflow/List/useWorkflowActions.ts
@@ -1,5 +1,6 @@
 import { computed, type Ref, ref } from "vue";
 
+import type { StoredWorkflowDetailed } from "@/api/workflows";
 import {
     copyWorkflow as copyWorkflowService,
     deleteWorkflow as deleteWorkflowService,
@@ -11,8 +12,7 @@ import { copy } from "@/utils/clipboard";
 import { withPrefix } from "@/utils/redirect";
 import { getFullAppUrl } from "@/utils/utils";
 
-// TODO: replace me with a more accurate type
-type Workflow = any;
+type Workflow = StoredWorkflowDetailed;
 
 export function useWorkflowActions(workflow: Ref<Workflow>, refreshCallback: () => void) {
     const toast = useToast();

From 479da196e88ea3376b606de5060dfb704cf617f8 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 19:07:24 +0100
Subject: [PATCH 087/131] Fixes import path and adds activity bar ID to
 NotificationItem

Corrects the import path for ActivityItem component
Adds activity-bar-id required prop to NotificationItem
---
 client/src/components/ActivityBar/Items/NotificationItem.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/src/components/ActivityBar/Items/NotificationItem.vue b/client/src/components/ActivityBar/Items/NotificationItem.vue
index 84e5a05bceda..e206dba5ecb2 100644
--- a/client/src/components/ActivityBar/Items/NotificationItem.vue
+++ b/client/src/components/ActivityBar/Items/NotificationItem.vue
@@ -4,7 +4,7 @@ import { computed } from "vue";
 
 import { useNotificationsStore } from "@/stores/notificationsStore";
 
-import ActivityItem from "components/ActivityBar/ActivityItem.vue";
+import ActivityItem from "@/components/ActivityBar/ActivityItem.vue";
 
 const { totalUnreadCount } = storeToRefs(useNotificationsStore());
 
@@ -31,6 +31,7 @@ const tooltip = computed(() =>
 <template>
     <ActivityItem
         :id="id"
+        :activity-bar-id="'notifications'"
         :icon="icon"
         :indicator="totalUnreadCount"
         :is-active="isActive"

From dc5fb053000d16e2617d8dc3a47829cc927e8caf Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 19:24:00 +0100
Subject: [PATCH 088/131] Adds activity-bar-id prop to ActivityItem components

Updates import path for ActivityItem in InteractiveItem.vue
Adds activity-bar-id prop to ActivityItem in InteractiveItem.vue and UploadItem.vue
---
 client/src/components/ActivityBar/Items/InteractiveItem.vue | 3 ++-
 client/src/components/ActivityBar/Items/UploadItem.vue      | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/src/components/ActivityBar/Items/InteractiveItem.vue b/client/src/components/ActivityBar/Items/InteractiveItem.vue
index 15aa604b3f13..acb2bac8b954 100644
--- a/client/src/components/ActivityBar/Items/InteractiveItem.vue
+++ b/client/src/components/ActivityBar/Items/InteractiveItem.vue
@@ -4,7 +4,7 @@ import { computed } from "vue";
 
 import { useEntryPointStore } from "@/stores/entryPointStore";
 
-import ActivityItem from "components/ActivityBar/ActivityItem.vue";
+import ActivityItem from "@/components/ActivityBar/ActivityItem.vue";
 
 const { entryPoints } = storeToRefs(useEntryPointStore());
 
@@ -35,6 +35,7 @@ const tooltip = computed(() =>
     <ActivityItem
         v-if="totalCount > 0"
         :id="id"
+        :activity-bar-id="id"
         :icon="icon"
         :indicator="totalCount"
         :is-active="isActive"
diff --git a/client/src/components/ActivityBar/Items/UploadItem.vue b/client/src/components/ActivityBar/Items/UploadItem.vue
index 2a5fbf7521db..69d2a1cb3d23 100644
--- a/client/src/components/ActivityBar/Items/UploadItem.vue
+++ b/client/src/components/ActivityBar/Items/UploadItem.vue
@@ -39,6 +39,7 @@ function onUploadModal() {
 <template>
     <ActivityItem
         :id="id"
+        :activity-bar-id="id"
         :title="title"
         :tooltip="tooltip"
         :icon="icon"

From 6d8c7e786250036410386fbf7a200a8c4b74ac38 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 20:10:51 +0100
Subject: [PATCH 089/131] Adds favorites filter functionality to WorkflowPanel

Integrates FavoritesButton component into WorkflowPanel
Implements computed property to manage bookmarked filter state
Updates filter text handling to support bookmarked filtering
---
 .../Panels/Buttons/FavoritesButton.vue        |  9 +++++--
 .../src/components/Panels/WorkflowPanel.vue   | 26 +++++++++++++++----
 2 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/client/src/components/Panels/Buttons/FavoritesButton.vue b/client/src/components/Panels/Buttons/FavoritesButton.vue
index 79941f3395f7..37d932b6fedd 100644
--- a/client/src/components/Panels/Buttons/FavoritesButton.vue
+++ b/client/src/components/Panels/Buttons/FavoritesButton.vue
@@ -15,9 +15,14 @@ library.add(faStar, faRegStar);
 interface Props {
     value?: boolean;
     query?: string;
+    tooltip?: string;
 }
 
-const props = defineProps<Props>();
+const props = withDefaults(defineProps<Props>(), {
+    value: false,
+    query: undefined,
+    tooltip: "Show favorites",
+});
 
 const currentValue = computed(() => props.value ?? false);
 const toggle = ref(false);
@@ -43,7 +48,7 @@ const tooltipText = computed(() => {
         if (toggle.value) {
             return "Clear";
         } else {
-            return "Show favorites";
+            return props.tooltip;
         }
     }
 });
diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index 670a12710e1f..ed59fc7c35a9 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -7,6 +7,7 @@ import { useAnimationFrameScroll } from "@/composables/sensors/animationFrameScr
 import { useToast } from "@/composables/toast";
 
 import ActivityPanel from "./ActivityPanel.vue";
+import FavoritesButton from "./Buttons/FavoritesButton.vue";
 import DelayedInput from "@/components/Common/DelayedInput.vue";
 import ScrollToTopButton from "@/components/ToolsList/ScrollToTopButton.vue";
 import WorkflowCardList from "@/components/Workflow/List/WorkflowCardList.vue";
@@ -32,6 +33,21 @@ const filterText = ref("");
 
 const workflows = ref<Workflow[]>([]);
 
+const showFavorites = computed({
+    get() {
+        return filterText.value.includes("is:bookmarked");
+    },
+    set(value) {
+        if (value) {
+            if (!filterText.value.includes("is:bookmarked")) {
+                filterText.value = `is:bookmarked ${filterText.value}`.trim();
+            }
+        } else {
+            filterText.value = filterText.value.replace("is:bookmarked", "").trim();
+        }
+    },
+});
+
 const loadWorkflowsOptions = {
     sortBy: "update_time",
     sortDesc: true,
@@ -102,7 +118,8 @@ function refresh() {
 
 watchImmediate(
     () => filterText.value,
-    () => {
+    (newFilterText) => {
+        showFavorites.value = newFilterText.includes("#favorites");
         resetWorkflows();
         fetchKey = filterText.value;
         load();
@@ -125,10 +142,9 @@ function scrollToTop() {
 
 <template>
     <ActivityPanel title="Workflows">
-        <!-- favorites Button disabled until workflows api is fixed -->
-        <!--template v-slot:header-buttons>
-            <FavoritesButton v-model="showFavorites"></FavoritesButton>
-        </template-->
+        <template v-slot:header-buttons>
+            <FavoritesButton v-model="showFavorites" tooltip="Show bookmarked" />
+        </template>
 
         <DelayedInput
             v-model="filterText"

From 31b6e71601c4244bd29edba33fbbbd2a037cba95 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 20:27:01 +0100
Subject: [PATCH 090/131] Refactors icon imports and usage

Updates icon imports to use `IconDefinition` type from `@fortawesome/fontawesome-svg-core`
Replaces string-based icon references with FontAwesome icon objects
Removes unnecessary `library.add` calls

Improves type safety and consistency in icon usage in activity bar components
---
 .../components/ActivityBar/ActivityBar.vue    |  7 ++--
 .../components/ActivityBar/ActivityItem.vue   |  6 ++-
 .../ActivityBar/ActivitySettings.vue          | 22 +++-------
 .../ActivityBar/Items/InteractiveItem.vue     |  3 +-
 .../ActivityBar/Items/NotificationItem.vue    |  3 +-
 .../ActivityBar/Items/UploadItem.vue          |  3 +-
 .../FormDrilldown/FormDrilldownOption.vue     |  3 +-
 client/src/stores/activitySetup.ts            | 42 +++++++++++++------
 client/src/stores/activityStore.ts            |  3 +-
 9 files changed, 53 insertions(+), 39 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 169b90eed2c5..c1875ea271f3 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -1,5 +1,6 @@
 <script setup lang="ts">
-import { faEllipsisH, type IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { faBell, faEllipsisH, faUserCog } from "@fortawesome/free-solid-svg-icons";
 import { watchImmediate } from "@vueuse/core";
 import { storeToRefs } from "pinia";
 import { computed, type Ref, ref } from "vue";
@@ -263,7 +264,7 @@ defineExpose({
                 <NotificationItem
                     v-if="isConfigLoaded && config.enable_notification_system"
                     id="activity-notifications"
-                    icon="bell"
+                    :icon="faBell"
                     :is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
                     title="Notifications"
                     @click="toggleSidebar('notifications')" />
@@ -279,7 +280,7 @@ defineExpose({
                     v-if="isAdmin && showAdmin"
                     id="admin"
                     :activity-bar-id="props.activityBarId"
-                    icon="user-cog"
+                    :icon="faUserCog"
                     :is-active="isActiveSideBar('admin')"
                     title="Admin"
                     tooltip="Administer this Galaxy"
diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index 279d91d10edc..ee2443bdc091 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { faQuestion } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import type { Placement } from "@popperjs/core";
 import { computed } from "vue";
@@ -21,7 +23,7 @@ export interface Props {
     id: string;
     activityBarId: string;
     title?: string;
-    icon?: string | object;
+    icon?: IconDefinition;
     indicator?: number;
     isActive?: boolean;
     tooltip?: string;
@@ -35,7 +37,7 @@ export interface Props {
 
 const props = withDefaults(defineProps<Props>(), {
     title: undefined,
-    icon: "question",
+    icon: () => faQuestion,
     indicator: 0,
     isActive: false,
     options: undefined,
diff --git a/client/src/components/ActivityBar/ActivitySettings.vue b/client/src/components/ActivityBar/ActivitySettings.vue
index d0c8943641ad..272c4304b938 100644
--- a/client/src/components/ActivityBar/ActivitySettings.vue
+++ b/client/src/components/ActivityBar/ActivitySettings.vue
@@ -1,22 +1,12 @@
 <script setup lang="ts">
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faSquare, faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
-import { faCheckSquare, faStar, faThumbtack, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
+import { faStar, faTrash } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { computed, type ComputedRef } from "vue";
 import { useRouter } from "vue-router/composables";
 
 import { type Activity, useActivityStore } from "@/stores/activityStore";
 
-library.add({
-    faCheckSquare,
-    faSquare,
-    faStar,
-    faStarRegular,
-    faTrash,
-    faThumbtack,
-});
-
 const props = defineProps<{
     activityBarId: string;
     query: string;
@@ -90,7 +80,7 @@ function executeActivity(activity: Activity) {
                 <div class="d-flex justify-content-between align-items-start">
                     <span class="d-flex justify-content-between w-100">
                         <span>
-                            <icon class="mr-1" :icon="activity.icon" />
+                            <FontAwesomeIcon class="mr-1" :icon="activity.icon" />
                             <span v-localize class="font-weight-bold">{{
                                 activity.title || "No title available"
                             }}</span>
@@ -104,7 +94,7 @@ function executeActivity(activity: Activity) {
                                 title="Delete Activity"
                                 variant="link"
                                 @click.stop="onRemove(activity)">
-                                <FontAwesomeIcon icon="fa-trash" fa-fw />
+                                <FontAwesomeIcon :icon="faTrash" fa-fw />
                             </BButton>
                             <BButton
                                 v-if="activity.visible"
@@ -113,7 +103,7 @@ function executeActivity(activity: Activity) {
                                 title="Hide in Activity Bar"
                                 variant="link"
                                 @click.stop="onFavorite(activity)">
-                                <FontAwesomeIcon icon="fas fa-star" fa-fw />
+                                <FontAwesomeIcon :icon="faStar" fa-fw />
                             </BButton>
                             <BButton
                                 v-else
@@ -122,7 +112,7 @@ function executeActivity(activity: Activity) {
                                 title="Show in Activity Bar"
                                 variant="link"
                                 @click.stop="onFavorite(activity)">
-                                <FontAwesomeIcon icon="far fa-star" fa-fw />
+                                <FontAwesomeIcon :icon="faStarRegular" fa-fw />
                             </BButton>
                         </div>
                     </span>
diff --git a/client/src/components/ActivityBar/Items/InteractiveItem.vue b/client/src/components/ActivityBar/Items/InteractiveItem.vue
index acb2bac8b954..e9e4756344a9 100644
--- a/client/src/components/ActivityBar/Items/InteractiveItem.vue
+++ b/client/src/components/ActivityBar/Items/InteractiveItem.vue
@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
@@ -13,7 +14,7 @@ const totalCount = computed(() => entryPoints.value.length);
 export interface Props {
     id: string;
     title: string;
-    icon: string | object;
+    icon: IconDefinition;
     isActive: boolean;
     to: string;
 }
diff --git a/client/src/components/ActivityBar/Items/NotificationItem.vue b/client/src/components/ActivityBar/Items/NotificationItem.vue
index e206dba5ecb2..1f1dde9110e3 100644
--- a/client/src/components/ActivityBar/Items/NotificationItem.vue
+++ b/client/src/components/ActivityBar/Items/NotificationItem.vue
@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
 import { storeToRefs } from "pinia";
 import { computed } from "vue";
 
@@ -11,7 +12,7 @@ const { totalUnreadCount } = storeToRefs(useNotificationsStore());
 export interface Props {
     id: string;
     title: string;
-    icon: string;
+    icon: IconDefinition;
     isActive: boolean;
 }
 
diff --git a/client/src/components/ActivityBar/Items/UploadItem.vue b/client/src/components/ActivityBar/Items/UploadItem.vue
index 69d2a1cb3d23..6d5a6df9da64 100644
--- a/client/src/components/ActivityBar/Items/UploadItem.vue
+++ b/client/src/components/ActivityBar/Items/UploadItem.vue
@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
 import { storeToRefs } from "pinia";
 import { onMounted } from "vue";
 
@@ -11,7 +12,7 @@ import ActivityItem from "@/components/ActivityBar/ActivityItem.vue";
 export interface Props {
     id: string;
     title: string;
-    icon: string | object;
+    icon: IconDefinition;
     tooltip: string;
 }
 
diff --git a/client/src/components/Form/Elements/FormDrilldown/FormDrilldownOption.vue b/client/src/components/Form/Elements/FormDrilldown/FormDrilldownOption.vue
index 858cee8cbe45..e403d43774c5 100644
--- a/client/src/components/Form/Elements/FormDrilldown/FormDrilldownOption.vue
+++ b/client/src/components/Form/Elements/FormDrilldown/FormDrilldownOption.vue
@@ -1,5 +1,6 @@
 <script setup lang="ts">
-import { faCaretDown, faCaretRight, faFile, faFolder, type IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { faCaretDown, faCaretRight, faFile, faFolder } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import { BFormCheckbox, BFormRadio } from "bootstrap-vue";
 import { computed, type ComputedRef, onMounted, ref } from "vue";
diff --git a/client/src/stores/activitySetup.ts b/client/src/stores/activitySetup.ts
index 80d3c0221161..af494b4b0409 100644
--- a/client/src/stores/activitySetup.ts
+++ b/client/src/stores/activitySetup.ts
@@ -1,6 +1,22 @@
 /**
  * List of built-in activities
  */
+import {
+    faChartBar,
+    faColumns,
+    faDatabase,
+    faFile,
+    faFileContract,
+    faFolder,
+    faHdd,
+    faLaptop,
+    faList,
+    faPlay,
+    faSitemap,
+    faUpload,
+    faWrench,
+} from "@fortawesome/free-solid-svg-icons";
+
 import { type Activity } from "@/stores/activityStore";
 import { type EventData } from "@/stores/eventStore";
 
@@ -8,7 +24,7 @@ export const defaultActivities = [
     {
         anonymous: false,
         description: "Displays currently running interactive tools (ITs), if these are enabled by the administrator.",
-        icon: "fa-laptop",
+        icon: faLaptop,
         id: "interactivetools",
         mutable: false,
         optional: false,
@@ -21,7 +37,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Opens a data dialog, allowing uploads from URL, pasted content or disk.",
-        icon: "upload",
+        icon: faUpload,
         id: "upload",
         mutable: false,
         optional: false,
@@ -34,7 +50,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Displays the tool panel to search and access all available tools.",
-        icon: "wrench",
+        icon: faWrench,
         id: "tools",
         mutable: false,
         optional: false,
@@ -47,7 +63,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Displays a panel to search and access workflows.",
-        icon: "sitemap",
+        icon: faSitemap,
         id: "workflows",
         mutable: false,
         optional: true,
@@ -60,7 +76,7 @@ export const defaultActivities = [
     {
         anonymous: false,
         description: "Displays all workflow runs.",
-        icon: "fa-list",
+        icon: faList,
         id: "invocation",
         mutable: false,
         optional: true,
@@ -73,7 +89,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Displays the list of available visualizations.",
-        icon: "chart-bar",
+        icon: faChartBar,
         id: "visualizations",
         mutable: false,
         optional: true,
@@ -86,7 +102,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Displays the list of all histories.",
-        icon: "fa-hdd",
+        icon: faHdd,
         id: "histories",
         mutable: false,
         optional: true,
@@ -99,7 +115,7 @@ export const defaultActivities = [
     {
         anonymous: false,
         description: "Displays the history selector panel and opens History Multiview in the center panel.",
-        icon: "fa-columns",
+        icon: faColumns,
         id: "multiview",
         mutable: false,
         optional: true,
@@ -112,7 +128,7 @@ export const defaultActivities = [
     {
         anonymous: false,
         description: "Displays all of your datasets across all histories.",
-        icon: "fa-folder",
+        icon: faFolder,
         id: "datasets",
         mutable: false,
         optional: true,
@@ -125,7 +141,7 @@ export const defaultActivities = [
     {
         anonymous: true,
         description: "Display and create new pages.",
-        icon: "fa-file-contract",
+        icon: faFileContract,
         id: "pages",
         mutable: false,
         optional: true,
@@ -138,7 +154,7 @@ export const defaultActivities = [
     {
         anonymous: false,
         description: "Display Data Libraries with datasets available to all users.",
-        icon: "fa-database",
+        icon: faDatabase,
         id: "libraries",
         mutable: false,
         optional: true,
@@ -155,7 +171,7 @@ export function convertDropData(data: EventData): Activity | null {
         return {
             anonymous: true,
             description: "Displays this dataset.",
-            icon: "fa-file",
+            icon: faFile,
             id: `dataset-${data.id}`,
             mutable: true,
             optional: true,
@@ -170,7 +186,7 @@ export function convertDropData(data: EventData): Activity | null {
         return {
             anonymous: false,
             description: data.description as string,
-            icon: "fa-play",
+            icon: faPlay,
             id: `workflow-${data.id}`,
             mutable: true,
             optional: true,
diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts
index 341b1a0e45dc..466ee226bf00 100644
--- a/client/src/stores/activityStore.ts
+++ b/client/src/stores/activityStore.ts
@@ -1,6 +1,7 @@
 /**
  * Stores the Activity Bar state
  */
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
 import { useDebounceFn, watchImmediate } from "@vueuse/core";
 import { computed, type Ref, ref, set } from "vue";
 
@@ -21,7 +22,7 @@ export interface Activity {
     // unique identifier
     id: string;
     // icon to be displayed in activity bar
-    icon: string | object;
+    icon: IconDefinition;
     // indicate if this activity can be modified and/or deleted
     mutable?: boolean;
     // indicate wether this activity can be disabled by the user

From e84ab9c0329a751aec80cef4088ee243998b49a7 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 20:46:56 +0100
Subject: [PATCH 091/131] Updates icon handling in tests

Replaces stubbed icon component with FontAwesomeIcon in ActivitySettings test
Changes icon property type to IconDefinition in activityStore test for type safety
---
 client/src/components/ActivityBar/ActivitySettings.test.js | 4 ++--
 client/src/stores/activityStore.test.ts                    | 7 ++++---
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivitySettings.test.js b/client/src/components/ActivityBar/ActivitySettings.test.js
index 873bdd958458..d40b35785650 100644
--- a/client/src/components/ActivityBar/ActivitySettings.test.js
+++ b/client/src/components/ActivityBar/ActivitySettings.test.js
@@ -50,7 +50,7 @@ describe("ActivitySettings", () => {
                 activityBarId: undefined,
             },
             stubs: {
-                icon: { template: "<div></div>" },
+                FontAwesomeIcon: { template: "<div></div>" },
             },
         });
         await activityStore.sync();
@@ -102,7 +102,7 @@ describe("ActivitySettings", () => {
         await wrapper.vm.$nextTick();
         const items = wrapper.findAll(activityItemSelector);
         expect(items.length).toBe(1);
-        const trash = items.at(0).find("[data-icon='trash']");
+        const trash = items.at(0).find("[data-description='delete activity']");
         expect(trash.exists()).toBeTruthy();
         expect(activityStore.getAll().length).toBe(1);
         trash.trigger("click");
diff --git a/client/src/stores/activityStore.test.ts b/client/src/stores/activityStore.test.ts
index c6c8461c4f2d..93eebee8e56b 100644
--- a/client/src/stores/activityStore.test.ts
+++ b/client/src/stores/activityStore.test.ts
@@ -1,3 +1,4 @@
+import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
 import { createPinia, setActivePinia } from "pinia";
 
 import { useActivityStore } from "@/stores/activityStore";
@@ -8,7 +9,7 @@ jest.mock("./activitySetup", () => ({
         {
             anonymous: false,
             description: "a-description",
-            icon: "a-icon",
+            icon: "a-icon" as unknown as IconDefinition,
             id: "a-id",
             mutable: false,
             optional: false,
@@ -25,7 +26,7 @@ const newActivities = [
     {
         anonymous: false,
         description: "a-description-new",
-        icon: "a-icon-new",
+        icon: "a-icon-new" as unknown as IconDefinition,
         id: "a-id",
         mutable: false,
         optional: false,
@@ -38,7 +39,7 @@ const newActivities = [
     {
         anonymous: false,
         description: "b-description-new",
-        icon: "b-icon-new",
+        icon: "b-icon-new" as unknown as IconDefinition,
         id: "b-id",
         mutable: true,
         optional: false,

From 0d791b97adcb3c6801fe03c19829bdcba900e386 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Fri, 15 Nov 2024 20:56:48 +0100
Subject: [PATCH 092/131] Updates filter keyword for workflow bookmarks

Fix the filter keyword from "#favorites" to "is:bookmarked"
---
 client/src/components/Panels/WorkflowPanel.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Panels/WorkflowPanel.vue b/client/src/components/Panels/WorkflowPanel.vue
index ed59fc7c35a9..d5e56f332d5c 100644
--- a/client/src/components/Panels/WorkflowPanel.vue
+++ b/client/src/components/Panels/WorkflowPanel.vue
@@ -119,7 +119,7 @@ function refresh() {
 watchImmediate(
     () => filterText.value,
     (newFilterText) => {
-        showFavorites.value = newFilterText.includes("#favorites");
+        showFavorites.value = newFilterText.includes("is:bookmarked");
         resetWorkflows();
         fetchKey = filterText.value;
         load();

From 3e1ff4860a68f53524106edf0476aa42fca03c27 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Sun, 17 Nov 2024 18:04:54 +0100
Subject: [PATCH 093/131] Simplifies activity item IDs

Removes redundant prefix from activity item IDs for consistency
Changes notification item ID to 'notifications' for clarity
---
 client/src/components/ActivityBar/ActivityBar.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index c1875ea271f3..7e9cbad7190e 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -218,14 +218,14 @@ defineExpose({
                         <div v-if="activity.visible && (activity.anonymous || !isAnonymous)">
                             <UploadItem
                                 v-if="activity.id === 'upload'"
-                                :id="`activity-${activity.id}`"
+                                :id="`${activity.id}`"
                                 :key="activity.id"
                                 :icon="activity.icon"
                                 :title="activity.title"
                                 :tooltip="activity.tooltip" />
                             <InteractiveItem
                                 v-else-if="activity.to && activity.id === 'interactivetools'"
-                                :id="`activity-${activity.id}`"
+                                :id="`${activity.id}`"
                                 :key="activity.id"
                                 :icon="activity.icon"
                                 :is-active="isActiveRoute(activity.to)"
@@ -263,7 +263,7 @@ defineExpose({
             <b-nav v-if="!isAnonymous" vertical class="activity-footer flex-nowrap p-1">
                 <NotificationItem
                     v-if="isConfigLoaded && config.enable_notification_system"
-                    id="activity-notifications"
+                    id="notifications"
                     :icon="faBell"
                     :is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
                     title="Notifications"

From 08572c26ccd584718b65957ebb75c4fc2a824489 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Sun, 17 Nov 2024 18:05:37 +0100
Subject: [PATCH 094/131] Fix activity item upload progress background

---
 client/src/components/ActivityBar/ActivityItem.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/client/src/components/ActivityBar/ActivityItem.vue b/client/src/components/ActivityBar/ActivityItem.vue
index ee2443bdc091..fa7e67d5e8c0 100644
--- a/client/src/components/ActivityBar/ActivityItem.vue
+++ b/client/src/components/ActivityBar/ActivityItem.vue
@@ -69,7 +69,7 @@ const meta = computed(() => store.metaForId(props.id));
         <template v-slot:reference>
             <b-nav-item
                 :id="`activity-${id}`"
-                class="activity-item position-relative my-1 p-2"
+                class="activity-item my-1 p-2"
                 :class="{ 'nav-item-active': isActive }"
                 :link-classes="`variant-${props.variant}`"
                 :aria-label="localize(title)"
@@ -113,6 +113,7 @@ const meta = computed(() => store.metaForId(props.id));
 @import "theme/blue.scss";
 
 .activity-item {
+    position: relative;
     display: flex;
     flex-direction: column;
 
@@ -126,6 +127,7 @@ const meta = computed(() => store.metaForId(props.id));
 }
 
 .nav-icon {
+    position: relative;
     display: flex;
     justify-content: center;
     cursor: pointer;
@@ -162,6 +164,7 @@ const meta = computed(() => store.metaForId(props.id));
 }
 
 .nav-title {
+    position: relative;
     display: flex;
     justify-content: center;
     width: 4rem;

From 2bdcb245c0716c6f172b627b5ed9c0c3deedade1 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Sun, 17 Nov 2024 18:09:10 +0100
Subject: [PATCH 095/131] Adds ID to toolbox panel

Adds an ID attribute to the toolbox panel.
Updates the tool opening logic in the workflow editor to open the toolbox panel using the activity bar before selecting tool
---
 client/src/components/Panels/ToolPanel.vue | 2 +-
 client/src/utils/navigation/navigation.yml | 1 +
 lib/galaxy/selenium/navigates_galaxy.py    | 3 +++
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue
index 01b9c9028cf3..fc9b9db2aa60 100644
--- a/client/src/components/Panels/ToolPanel.vue
+++ b/client/src/components/Panels/ToolPanel.vue
@@ -152,7 +152,7 @@ watch(
 </script>
 
 <template>
-    <div v-if="arePanelsFetched" class="unified-panel" aria-labelledby="toolbox-heading">
+    <div v-if="arePanelsFetched" id="toolbox-panel" class="unified-panel" aria-labelledby="toolbox-heading">
         <div unselectable="on">
             <div class="unified-panel-header-inner mx-3 my-2 d-flex justify-content-between">
                 <PanelViewMenu
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 3d71b874ae14..8543f6a3ffd3 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -894,6 +894,7 @@ tour:
 tools:
   selectors:
     activity: '#activity-tools'
+    workflows_activity: '#activity-workflow-editor-tools'
     body: '#center #tool-card-body'
     execute: '#execute'
     help: 'div.form-help'
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 79d3e9e7640c..7d323ce2ec13 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1644,6 +1644,9 @@ def invocation_index_table_elements(self):
         return invocations.invocations_table_rows.all()
 
     def tool_open(self, tool_id, outer=False):
+        if self.wait_for_selector_absent_or_hidden("#toolbox-panel", wait_type=WAIT_TYPES.UX_RENDER):
+            self.components.tools.workflows_activity.wait_for_and_click()
+
         if outer:
             tool_link = self.components.tool_panel.outer_tool_link(tool_id=tool_id)
         else:

From 2fad3b5587b8e978148ff6a4301c87e5881fb831 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Sun, 17 Nov 2024 18:10:51 +0100
Subject: [PATCH 096/131] Adds auto-layout button ID in workflow toolbar

Introduces an ID for the auto-layout button in the toolbar component.
Updates navigation configuration and Selenium tests to use the new button ID for auto-layout functionality.
---
 .../src/components/Workflow/Editor/Tools/ToolBar.vue   |  1 +
 client/src/utils/navigation/navigation.yml             |  1 +
 lib/galaxy_test/selenium/test_workflow_editor.py       | 10 +++++-----
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Tools/ToolBar.vue b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
index 0f2c38d7b086..0df88909f6fe 100644
--- a/client/src/components/Workflow/Editor/Tools/ToolBar.vue
+++ b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
@@ -250,6 +250,7 @@ function autoLayout() {
                 </BButton>
 
                 <BButton
+                    id="auto-layout-button"
                     v-b-tooltip.hover.noninteractive.right
                     title="Auto Layout (Ctrl + 9)"
                     data-tool="auto_layout"
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 8543f6a3ffd3..bd8b1e877514 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -732,6 +732,7 @@ workflow_editor:
       clear_selection: "[title='clear selection']"
       duplicate_selection: "[title='duplicate selected']"
       delete_selection: "[title='delete selected']"
+      auto_layout: "#auto-layout-button"
   comment:
     selectors:
       _: ".workflow-editor-comment"
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index d07a00d8af51..2242be3834b9 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -795,7 +795,9 @@ def test_editor_create_conditional_step(self):
         editor.label_input.wait_for_and_send_keys("downstream_step")
         # Insert head tool
         self.tool_open("head")
-        self.workflow_editor_click_option("Auto Layout")
+
+        self.components.workflow_editor.tool_bar.auto_layout.wait_for_and_click()
+
         self.sleep_for(self.wait_types.UX_RENDER)
         editor.label_input.wait_for_and_send_keys("conditional_step")
         # Connect head to cat
@@ -845,7 +847,7 @@ def test_conditional_subworkflow_step(self):
         param_type_element = editor.param_type_form.wait_for_present()
         self.switch_param_type(param_type_element, "Boolean")
         editor.label_input.wait_for_and_send_keys("param_input")
-        self.workflow_editor_click_option("Auto Layout")
+        self.components.workflow_editor.tool_bar.auto_layout.wait_for_and_click()
         self.sleep_for(self.wait_types.UX_RENDER)
         conditional_node = editor.node._(label=child_workflow_name)
         conditional_node.wait_for_and_click()
@@ -1219,8 +1221,6 @@ def test_editor_snapping(self):
         self.workflow_create_new(annotation="simple workflow")
         self.sleep_for(self.wait_types.UX_RENDER)
 
-        editor.tool_menu.wait_for_visible()
-
         self.tool_open("cat")
         self.sleep_for(self.wait_types.UX_RENDER)
         editor.label_input.wait_for_and_send_keys("tool_node")
@@ -1417,7 +1417,7 @@ def open_in_workflow_editor(self, yaml_content, auto_layout=True):
         self.workflow_index_open()
         self.workflow_index_open_with_name(name)
         if auto_layout:
-            self.workflow_editor_click_option("Auto Layout")
+            self.components.workflow_editor.tool_bar.auto_layout.wait_for_and_click()
             self.sleep_for(self.wait_types.UX_RENDER)
         return name
 

From 68d88ae4b5e4c81a62af4e1d5242d511f4692269 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Mon, 18 Nov 2024 14:46:22 +0100
Subject: [PATCH 097/131] Adds hidePanel prop to ActivityBar and integrates
 MarkdownToolBox in MarkdownEditor

Introduces hidePanel prop to conditionally hide the side panel in ActivityBar.
Integrates MarkdownToolBox into MarkdownEditor temporary to have MarkdownToolBox in the page editor too.
---
 client/src/components/ActivityBar/ActivityBar.vue | 4 +++-
 client/src/components/Markdown/MarkdownEditor.vue | 7 +++++++
 client/src/components/Workflow/Editor/Index.vue   | 5 ++---
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 7e9cbad7190e..5371d14f84fe 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -38,6 +38,7 @@ const props = withDefaults(
         optionsIcon?: IconDefinition;
         optionsSearchPlaceholder?: string;
         initialActivity?: string;
+        hidePanel?: boolean;
     }>(),
     {
         defaultActivities: undefined,
@@ -50,6 +51,7 @@ const props = withDefaults(
         optionsSearchPlaceholder: "Search Activities",
         optionsTooltip: "View additional activities",
         initialActivity: undefined,
+        hidePanel: false,
     }
 );
 
@@ -314,7 +316,7 @@ defineExpose({
                 </template>
             </b-nav>
         </div>
-        <FlexPanel v-if="isSideBarOpen" side="left" :collapsible="false">
+        <FlexPanel v-if="isSideBarOpen && !hidePanel" side="left" :collapsible="false">
             <ToolPanel v-if="isActiveSideBar('tools')" />
             <InvocationsPanel v-else-if="isActiveSideBar('invocation')" :activity-bar-id="props.activityBarId" />
             <VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
diff --git a/client/src/components/Markdown/MarkdownEditor.vue b/client/src/components/Markdown/MarkdownEditor.vue
index 69a2bec4f265..0ace2e5a3b6d 100644
--- a/client/src/components/Markdown/MarkdownEditor.vue
+++ b/client/src/components/Markdown/MarkdownEditor.vue
@@ -1,5 +1,8 @@
 <template>
     <div id="columns" class="d-flex">
+        <FlexPanel side="left">
+            <MarkdownToolBox :steps="steps" @insert="insertMarkdown" />
+        </FlexPanel>
         <div id="center" class="overflow-auto w-100">
             <div class="markdown-editor h-100">
                 <div class="unified-panel-header" unselectable="on">
@@ -39,10 +42,12 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 import { faQuestion } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import BootstrapVue from "bootstrap-vue";
+import FlexPanel from "components/Panels/FlexPanel";
 import _ from "underscore";
 import Vue from "vue";
 
 import MarkdownHelpModal from "./MarkdownHelpModal";
+import MarkdownToolBox from "./MarkdownToolBox";
 
 Vue.use(BootstrapVue);
 
@@ -52,8 +57,10 @@ const FENCE = "```";
 
 export default {
     components: {
+        FlexPanel,
         FontAwesomeIcon,
         MarkdownHelpModal,
+        MarkdownToolBox,
     },
     props: {
         markdownText: {
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 085a7cc7ce99..67a8c845c40c 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -39,6 +39,7 @@
             options-search-placeholder="Search options"
             initial-activity="workflow-editor-attributes"
             :options-icon="faCog"
+            :hide-panel="reportActive"
             @activityClicked="onActivityClicked">
             <template v-slot:side-panel="{ isActiveSideBar }">
                 <ToolPanel
@@ -87,7 +88,6 @@
                     @creator="onCreator"
                     @update:nameCurrent="setName"
                     @update:annotationCurrent="setAnnotation" />
-                <MarkdownToolBox v-else-if="isActiveSideBar('workflow-editor-report')" @insert="insertMarkdown" />
             </template>
         </ActivityBar>
         <template v-if="reportActive">
@@ -97,6 +97,7 @@
                 mode="report"
                 :title="'Workflow Report: ' + name"
                 :steps="steps"
+                @insert="insertMarkdown"
                 @update="onReportUpdate">
                 <template v-slot:buttons>
                     <b-button
@@ -221,7 +222,6 @@ import WorkflowAttributes from "./WorkflowAttributes.vue";
 import WorkflowGraph from "./WorkflowGraph.vue";
 import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
 import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
-import MarkdownToolBox from "@/components/Markdown/MarkdownToolBox.vue";
 import ToolPanel from "@/components/Panels/ToolPanel.vue";
 import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue";
 import UndoRedoStack from "@/components/UndoRedo/UndoRedoStack.vue";
@@ -243,7 +243,6 @@ export default {
         FontAwesomeIcon,
         UndoRedoStack,
         WorkflowPanel,
-        MarkdownToolBox,
         NodeInspector,
     },
     props: {

From cb322e537b126ef9cc4a5b022a89609360235445 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 18 Nov 2024 16:18:33 +0100
Subject: [PATCH 098/131] remove wait for absent element

---
 lib/galaxy/selenium/navigates_galaxy.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 7d323ce2ec13..1c76fd6a18f5 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1644,7 +1644,7 @@ def invocation_index_table_elements(self):
         return invocations.invocations_table_rows.all()
 
     def tool_open(self, tool_id, outer=False):
-        if self.wait_for_selector_absent_or_hidden("#toolbox-panel", wait_type=WAIT_TYPES.UX_RENDER):
+        if self.element_absent("#toolbox_panel"):
             self.components.tools.workflows_activity.wait_for_and_click()
 
         if outer:

From 9979a137207379c21814e8244b3f1853ffdb3edd Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 18 Nov 2024 17:10:42 +0100
Subject: [PATCH 099/131] fix add_input

---
 client/src/utils/navigation/navigation.yml       |  1 +
 lib/galaxy/selenium/navigates_galaxy.py          | 10 ++++++++--
 lib/galaxy_test/selenium/test_workflow_editor.py |  4 +---
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index bd8b1e877514..ae78df90b20b 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -20,6 +20,7 @@ _:  # global stuff
     right_panel_collapse: '.collapse-button.right'
     by_attribute: '${scope} [${name}="${value}"]'
     active_nav_item: '.nav-item-active'
+    toolbox_panel: '#toolbox_panel'
 
     confirm_button:
       type: xpath
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 1c76fd6a18f5..f6f4a3d3ba52 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1643,9 +1643,15 @@ def invocation_index_table_elements(self):
         invocations.invocations_table.wait_for_visible()
         return invocations.invocations_table_rows.all()
 
-    def tool_open(self, tool_id, outer=False):
-        if self.element_absent("#toolbox_panel"):
+    def open_toolbox(self):
+        self.sleep_for(self.wait_types.UX_RENDER)
+
+        if self.element_absent(self.components._.toolbox_panel):
             self.components.tools.workflows_activity.wait_for_and_click()
+            self.sleep_for(self.wait_types.UX_RENDER)
+
+    def tool_open(self, tool_id, outer=False):
+        self.open_toolbox()
 
         if outer:
             tool_link = self.components.tool_panel.outer_tool_link(tool_id=tool_id)
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 2242be3834b9..4bd936862c31 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -770,7 +770,6 @@ def test_editor_insert_steps(self):
         self.workflow_editor_add_input(item_name="data_input")
         editor = self.components.workflow_editor
         editor.canvas_body.wait_for_visible()
-        editor.tool_menu.wait_for_visible()
         editor.tool_menu_section_link(section_name="workflows").wait_for_and_click()
         editor.insert_steps(workflow_title=steps_to_insert).wait_for_and_click()
         self.assert_connected("input1#output", "first_cat#input1")
@@ -788,7 +787,6 @@ def test_editor_create_conditional_step(self):
         param_type_element = editor.param_type_form.wait_for_present()
         self.switch_param_type(param_type_element, "Boolean")
         editor.label_input.wait_for_and_send_keys("param_input")
-        editor.tool_menu.wait_for_visible()
         # Insert cat tool
         self.tool_open("cat")
         self.sleep_for(self.wait_types.UX_RENDER)
@@ -1450,7 +1448,7 @@ def workflow_editor_add_input(self, item_name="data_input"):
         # Make sure we're on the workflow editor and not clicking the main tool panel.
         editor.canvas_body.wait_for_visible()
 
-        editor.tool_menu.wait_for_visible()
+        self.open_toolbox()
         editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
         editor.tool_menu_item_link(item_name=item_name).wait_for_and_click()
 

From 0af713cf05930cf4ba0cd8628c2074dad0071768 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 18 Nov 2024 17:53:15 +0100
Subject: [PATCH 100/131] fix inserting steps

---
 client/src/utils/navigation/navigation.yml      |  1 +
 lib/galaxy/selenium/navigates_galaxy.py         | 17 +++++++++++++++++
 .../selenium/test_workflow_editor.py            |  3 +--
 3 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index ae78df90b20b..3d5589e8d985 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -839,6 +839,7 @@ workflow_editor:
     save_workflow_confirmation_button: '#save-workflow-confirmation .btn-primary'
     state_modal_body: '.state-upgrade-modal'
     modal_button_continue: '.modal-footer .btn'
+    workflow_activity: '#activity-workflow-editor-workflows'
 
 workflow_show:
   selectors:
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index f6f4a3d3ba52..9f8b53cc3606 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1243,6 +1243,23 @@ def workflow_editor_click_save(self):
         self.wait_for_and_click_selector("#workflow-save-button")
         self.sleep_for(self.wait_types.DATABASE_OPERATION)
 
+    def workflow_editor_search_for_workflow(self, name: str):
+        self.wait_for_and_click(self.components.workflow_editor.workflow_activity)
+        self.sleep_for(self.wait_types.UX_RENDER)
+
+        input = self.wait_for_selector(".activity-panel input")
+        input.send_keys(name)
+
+        self.sleep_for(self.wait_types.UX_RENDER)
+
+    def workflow_editor_add_steps(self, name: str):
+        self.workflow_editor_search_for_workflow(name)
+
+        insert_button = self.wait_for_selector(".activity-panel button[title='Copy steps into workflow']")
+        insert_button.click()
+
+        self.sleep_for(self.wait_types.UX_RENDER)
+
     def navigate_to_histories_page(self):
         self.home()
         self.components.histories.activity.wait_for_and_click()
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 4bd936862c31..f71c8fe471ad 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -770,8 +770,7 @@ def test_editor_insert_steps(self):
         self.workflow_editor_add_input(item_name="data_input")
         editor = self.components.workflow_editor
         editor.canvas_body.wait_for_visible()
-        editor.tool_menu_section_link(section_name="workflows").wait_for_and_click()
-        editor.insert_steps(workflow_title=steps_to_insert).wait_for_and_click()
+        self.workflow_editor_add_steps(steps_to_insert)
         self.assert_connected("input1#output", "first_cat#input1")
         self.assert_workflow_has_changes_and_save()
         workflow_id = self.driver.current_url.split("id=")[1]

From 0cab2664a46868536a5a13f02974f7e3dea50345 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 18 Nov 2024 19:52:20 +0100
Subject: [PATCH 101/131] fix tool menu container occurrences

---
 lib/galaxy/selenium/navigates_galaxy.py                 | 8 ++++++++
 lib/galaxy_test/selenium/test_workflow_editor.py        | 5 +----
 test/integration_selenium/test_edam_tool_panel_views.py | 2 +-
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 9f8b53cc3606..17a0b8a64faf 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1260,6 +1260,14 @@ def workflow_editor_add_steps(self, name: str):
 
         self.sleep_for(self.wait_types.UX_RENDER)
 
+    def workflow_editor_add_subworkflow(self, name: str):
+        self.workflow_editor_search_for_workflow(name)
+
+        insert_button = self.wait_for_selector(".activity-panel button[title='Insert as sub-workflow']")
+        insert_button.click()
+
+        self.sleep_for(self.wait_types.UX_RENDER)
+
     def navigate_to_histories_page(self):
         self.home()
         self.components.histories.activity.wait_for_and_click()
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index f71c8fe471ad..33a66b8c68cd 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -42,7 +42,6 @@ def test_basics(self):
         self.assert_wf_annotation_is(annotation)
 
         editor.canvas_body.wait_for_visible()
-        editor.tool_menu.wait_for_visible()
 
         # shouldn't have changes on fresh load
         save_button = self.components.workflow_editor.save_button
@@ -744,9 +743,7 @@ def setup_subworkflow(self):
         self.components.workflows.edit_button.wait_for_and_click()
         editor = self.components.workflow_editor
         editor.canvas_body.wait_for_visible()
-        editor.tool_menu.wait_for_visible()
-        editor.tool_menu_section_link(section_name="workflows").wait_for_and_click()
-        editor.workflow_link(workflow_title=child_workflow_name).wait_for_and_click()
+        self.workflow_editor_add_subworkflow(child_workflow_name)
         self.sleep_for(self.wait_types.UX_RENDER)
         self.assert_workflow_has_changes_and_save()
         workflow = self.workflow_populator.download_workflow(parent_workflow_id)
diff --git a/test/integration_selenium/test_edam_tool_panel_views.py b/test/integration_selenium/test_edam_tool_panel_views.py
index a98b608d2e6a..5155e84e174e 100644
--- a/test/integration_selenium/test_edam_tool_panel_views.py
+++ b/test/integration_selenium/test_edam_tool_panel_views.py
@@ -28,7 +28,7 @@ def test_basic_navigation(self):
 
         editor = self.components.workflow_editor
         editor.canvas_body.wait_for_visible()
-        editor.tool_menu.wait_for_visible()
+        self.open_toolbox()
         self._assert_displaying_edam_operations()
         self.screenshot("tool_panel_view_edam_workflow_editor")
 

From 4f38eca960b2f57262c3a79d13555e72ae6154a6 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Mon, 18 Nov 2024 23:36:38 +0100
Subject: [PATCH 102/131] Migrates WorkflowAttributes test to TypeScript and
 updates dependencies

Renames WorkflowAttributes.test.js to WorkflowAttributes.test.ts
Replaces createLocalVue with getLocalVue and adds Pinia setup
Mocks userTagsStore methods and updates test assertions
---
 ...tes.test.js => WorkflowAttributes.test.ts} | 43 ++++++++++++-------
 1 file changed, 28 insertions(+), 15 deletions(-)
 rename client/src/components/Workflow/Editor/{WorkflowAttributes.test.js => WorkflowAttributes.test.ts} (67%)

diff --git a/client/src/components/Workflow/Editor/WorkflowAttributes.test.js b/client/src/components/Workflow/Editor/WorkflowAttributes.test.ts
similarity index 67%
rename from client/src/components/Workflow/Editor/WorkflowAttributes.test.js
rename to client/src/components/Workflow/Editor/WorkflowAttributes.test.ts
index 8f74ae81d869..8b9f0eb013b4 100644
--- a/client/src/components/Workflow/Editor/WorkflowAttributes.test.js
+++ b/client/src/components/Workflow/Editor/WorkflowAttributes.test.ts
@@ -1,5 +1,8 @@
-import { createLocalVue, mount } from "@vue/test-utils";
+import { getLocalVue } from "@tests/jest/helpers";
+import { mount } from "@vue/test-utils";
 import { isDate } from "date-fns";
+import flushPromises from "flush-promises";
+import { createPinia, setActivePinia } from "pinia";
 
 import { useUserTagsStore } from "@/stores/userTagsStore";
 
@@ -17,21 +20,19 @@ const TEST_VERSIONS = [
 ];
 const autocompleteTags = ["#named_uer_tag", "abc", "my_tag"];
 
-jest.mock("@/stores/userTagsStore");
-useUserTagsStore.mockReturnValue({
-    userTags: autocompleteTags,
-    onNewTagSeen: jest.fn(),
-    onTagUsed: jest.fn(),
-    onMultipleNewTagsSeen: jest.fn(),
-});
-
 describe("WorkflowAttributes", () => {
     it("test attributes", async () => {
-        const localVue = createLocalVue();
+        const pinia = createPinia();
+        const localVue = getLocalVue(true);
+
+        setActivePinia(pinia);
+
         const untypedParameters = new UntypedParameters();
+
         untypedParameters.getParameter("workflow_parameter_0");
         untypedParameters.getParameter("workflow_parameter_1");
-        const wrapper = mount(WorkflowAttributes, {
+
+        const wrapper = mount(WorkflowAttributes as object, {
             propsData: {
                 id: "workflow_id",
                 name: TEST_NAME,
@@ -45,18 +46,30 @@ describe("WorkflowAttributes", () => {
                 LicenseSelector: true,
             },
             localVue,
+            pinia,
         });
+
+        await flushPromises();
+
+        const userTagsStore = useUserTagsStore();
+        jest.spyOn(userTagsStore, "userTags", "get").mockReturnValue(autocompleteTags);
+        userTagsStore.onNewTagSeen = jest.fn();
+        userTagsStore.onTagUsed = jest.fn();
+        userTagsStore.onMultipleNewTagsSeen = jest.fn();
+
         expect(wrapper.find(`[itemprop='description']`).attributes("content")).toBe(TEST_ANNOTATION);
         expect(wrapper.find(`[itemprop='name']`).attributes("content")).toBe(TEST_NAME);
         expect(wrapper.find(`#workflow-version-area > select`).exists()).toBeTruthy();
 
         const name = wrapper.find("#workflow-name");
-        expect(name.element.value).toBe(TEST_NAME);
+        expect((name.element as HTMLInputElement).value).toBe(TEST_NAME);
         await wrapper.setProps({ name: "new_workflow_name" });
-        expect(name.element.value).toBe("new_workflow_name");
+        expect((name.element as HTMLInputElement).value).toBe("new_workflow_name");
+
+        const version = wrapper.findAll(`#workflow-version-area > select > option`);
 
-        const version = wrapper.findAllComponents(`#workflow-version-area > select > option`);
         expect(version).toHaveLength(TEST_VERSIONS.length);
+
         for (let i = 0; i < version.length; i++) {
             const versionLabel = version.at(i).text();
             const versionDate = versionLabel.substring(versionLabel.indexOf(":") + 1, versionLabel.indexOf(",")).trim();
@@ -67,6 +80,6 @@ describe("WorkflowAttributes", () => {
         expect(parameters.length).toBe(2);
         expect(parameters.at(0).text()).toBe("1: workflow_parameter_0");
         expect(parameters.at(1).text()).toBe("2: workflow_parameter_1");
-        expect(wrapper.find("#workflow-annotation").element.value).toBe(TEST_ANNOTATION);
+        expect((wrapper.find("#workflow-annotation").element as HTMLInputElement).value).toBe(TEST_ANNOTATION);
     });
 });

From 9524aab143696dafca043e2ead9af00541732d67 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Tue, 19 Nov 2024 16:11:22 +0100
Subject: [PATCH 103/131] fix workflows activity id

---
 client/src/utils/navigation/navigation.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 3d5589e8d985..430597f1b757 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -897,7 +897,7 @@ tour:
 tools:
   selectors:
     activity: '#activity-tools'
-    workflows_activity: '#activity-workflow-editor-tools'
+    workflows_activity: '#activity-workflow-editor-workflows'
     body: '#center #tool-card-body'
     execute: '#execute'
     help: 'div.form-help'

From 8f1508b254a8508c0e3848b0d17a57fd05b87577 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Tue, 19 Nov 2024 16:21:25 +0100
Subject: [PATCH 104/131] Migrates ToolPanel test to TypeScript

Updates imports to use TypeScript-compatible paths and extensions
Adjusts mock configuration and type assertions for TypeScript
---
 .../{ToolPanel.test.js => ToolPanel.test.ts}  | 27 ++++++++++---------
 1 file changed, 15 insertions(+), 12 deletions(-)
 rename client/src/components/Panels/{ToolPanel.test.js => ToolPanel.test.ts} (84%)

diff --git a/client/src/components/Panels/ToolPanel.test.js b/client/src/components/Panels/ToolPanel.test.ts
similarity index 84%
rename from client/src/components/Panels/ToolPanel.test.js
rename to client/src/components/Panels/ToolPanel.test.ts
index 7b5575f251c9..1541fa67be4e 100644
--- a/client/src/components/Panels/ToolPanel.test.js
+++ b/client/src/components/Panels/ToolPanel.test.ts
@@ -3,27 +3,28 @@ import "jest-location-mock";
 import { mount } from "@vue/test-utils";
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
-import toolsList from "components/ToolsView/testData/toolsList";
-import toolsListInPanel from "components/ToolsView/testData/toolsListInPanel";
 import flushPromises from "flush-promises";
 import { createPinia } from "pinia";
 import { getLocalVue } from "tests/jest/helpers";
 
-import { useConfig } from "@/composables/config";
+import toolsList from "@/components/ToolsView/testData/toolsList.json";
+import toolsListInPanel from "@/components/ToolsView/testData/toolsListInPanel.json";
 
-import viewsList from "./testData/viewsList";
-import ToolPanel from "./ToolPanel";
+import viewsList from "./testData/viewsList.json";
 import { types_to_icons } from "./utilities";
 
+import ToolPanel from "./ToolPanel.vue";
+
 const localVue = getLocalVue();
 
 const TEST_PANELS_URI = "/api/tool_panels";
 
-jest.mock("composables/config");
-useConfig.mockReturnValue({
-    config: {},
-    isConfigLoaded: true,
-});
+jest.mock("@/composables/config", () => ({
+    useConfig: jest.fn(() => ({
+        config: {},
+        isConfigLoaded: true,
+    })),
+}));
 
 describe("ToolPanel", () => {
     it("test navigation of tool panel views menu", async () => {
@@ -37,7 +38,7 @@ describe("ToolPanel", () => {
             .reply(200, { default_panel_view: "default", views: viewsList });
 
         const pinia = createPinia();
-        const wrapper = mount(ToolPanel, {
+        const wrapper = mount(ToolPanel as object, {
             propsData: {
                 workflow: false,
                 editorWorkflows: null,
@@ -88,7 +89,9 @@ describe("ToolPanel", () => {
 
                 // Test: check if the panel header now has an icon and a changed name
                 const panelViewIcon = wrapper.find("[data-description='panel view header icon']");
-                expect(panelViewIcon.classes()).toContain(`fa-${types_to_icons[value.view_type]}`);
+                expect(panelViewIcon.classes()).toContain(
+                    `fa-${types_to_icons[value.view_type as keyof typeof types_to_icons]}`
+                );
                 expect(wrapper.find("#toolbox-heading").text()).toBe(value.name);
             } else {
                 // Test: check if the default panel view is already selected, and no icon

From afee2e78427d7b9a6283dc507106c58c525518b7 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Tue, 19 Nov 2024 23:17:17 +0100
Subject: [PATCH 105/131] Updates toolbox panel selectors and workflow editor
 logic

Renames toolbox panel selector for consistency
Adds new selector for workflow editor tools activity
Modifies logic to handle toolbox panel visibility and search functionality
---
 client/src/utils/navigation/navigation.yml |  5 +++--
 lib/galaxy/selenium/navigates_galaxy.py    | 13 ++++++++++---
 2 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 430597f1b757..594a68ffb8e1 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -20,7 +20,7 @@ _:  # global stuff
     right_panel_collapse: '.collapse-button.right'
     by_attribute: '${scope} [${name}="${value}"]'
     active_nav_item: '.nav-item-active'
-    toolbox_panel: '#toolbox_panel'
+    toolbox_panel: '#toolbox-panel'
 
     confirm_button:
       type: xpath
@@ -897,11 +897,12 @@ tour:
 tools:
   selectors:
     activity: '#activity-tools'
+    tools_activity_workflow_editor: '#activity-workflow-editor-tools'
     workflows_activity: '#activity-workflow-editor-workflows'
     body: '#center #tool-card-body'
     execute: '#execute'
     help: 'div.form-help'
-    search: '.search-query'
+    search: '#toolbox-panel .search-query'
     title: '.toolSectionTitle'
 
 admin:
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 17a0b8a64faf..b3a5dceb7d5f 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1671,13 +1671,20 @@ def invocation_index_table_elements(self):
     def open_toolbox(self):
         self.sleep_for(self.wait_types.UX_RENDER)
 
-        if self.element_absent(self.components._.toolbox_panel):
-            self.components.tools.workflows_activity.wait_for_and_click()
-            self.sleep_for(self.wait_types.UX_RENDER)
+        if self.element_absent(self.components.tools.tools_activity_workflow_editor):
+            if self.element_absent(self.components._.toolbox_panel):
+                self.components.tools.activity.wait_for_and_click()
+        else:
+            if self.element_absent(self.components._.toolbox_panel):
+                self.components.tools.tools_activity_workflow_editor.wait_for_and_click()
+
+        self.sleep_for(self.wait_types.UX_RENDER)
 
     def tool_open(self, tool_id, outer=False):
         self.open_toolbox()
 
+        self.components.tools.search.wait_for_and_send_keys(f"id:{tool_id}")
+
         if outer:
             tool_link = self.components.tool_panel.outer_tool_link(tool_id=tool_id)
         else:

From 257d2cbafa7184d1d5bb250c0fb69e9f17b5b692 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Tue, 19 Nov 2024 23:36:58 +0100
Subject: [PATCH 106/131] Adds 'Save As' functionality to workflow editor

---
 client/src/components/Workflow/Editor/Index.vue | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 67a8c845c40c..408282998b9b 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -755,6 +755,10 @@ export default {
             if (activityId === "save-workflow") {
                 await this.saveOrCreate();
             }
+
+            if (activityId === "save-workflow-as") {
+                this.onSaveAs();
+            }
         },
         onAnnotation(nodeId, newAnnotation) {
             this.stepActions.setAnnotation(this.steps[nodeId], newAnnotation);

From f2a173f871d08a70b7169aa4b8ad67c64c6658f1 Mon Sep 17 00:00:00 2001
From: Alireza Heidari <itisalirh@gmail.com>
Date: Tue, 19 Nov 2024 23:37:19 +0100
Subject: [PATCH 107/131] Adds 'Save As' option to workflow editor toolbar

Includes a new selector for the 'Save As' option in the navigation configuration.
Updates the Selenium test to handle the 'Save As' option visibility and interaction.

Improves user experience by ensuring the 'Save As' option is accessible and testable.
---
 client/src/utils/navigation/navigation.yml       | 1 +
 lib/galaxy_test/selenium/test_workflow_editor.py | 6 +++++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 594a68ffb8e1..377de4e5c0b8 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -734,6 +734,7 @@ workflow_editor:
       duplicate_selection: "[title='duplicate selected']"
       delete_selection: "[title='delete selected']"
       auto_layout: "#auto-layout-button"
+      option_save_as: ".activity-settings-item[contains(text(), 'Save as')]"
   comment:
     selectors:
       _: ".workflow-editor-comment"
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 33a66b8c68cd..df982b092037 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -472,7 +472,11 @@ def test_save_as(self):
         self.workflow_index_open_with_name(name)
         self.sleep_for(self.wait_types.UX_RENDER)
         self.screenshot("workflow_editor_edit_menu")
-        self.workflow_editor_click_option("Save As")
+
+        if self.element_absent(self.components.workflow_editor.tool_bar.option_save_as):
+            self.components.preferences.activity.wait_for_and_click()
+
+        self.components.workflow_editor.tool_bar.option_save_as.wait_for_and_click()
 
     @selenium_test
     def test_editor_tool_upgrade(self):

From d25f9eb30cba505a624aa13c2201448ff6f104b9 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 20:42:39 +0100
Subject: [PATCH 108/131] fix basics test

---
 lib/galaxy_test/selenium/test_workflow_editor.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index df982b092037..d9997dbfa26e 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -45,8 +45,7 @@ def test_basics(self):
 
         # shouldn't have changes on fresh load
         save_button = self.components.workflow_editor.save_button
-        save_button.wait_for_visible()
-        assert save_button.has_class("disabled")
+        assert save_button.is_absent
 
         self.screenshot("workflow_editor_blank")
 

From 1f611166a706564af9604a6d19351986a5c85563 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 20:47:45 +0100
Subject: [PATCH 109/131] revert shortening of ids

---
 client/src/components/ActivityBar/ActivityBar.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index 5371d14f84fe..a9d8151280b7 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -265,13 +265,13 @@ defineExpose({
             <b-nav v-if="!isAnonymous" vertical class="activity-footer flex-nowrap p-1">
                 <NotificationItem
                     v-if="isConfigLoaded && config.enable_notification_system"
-                    id="notifications"
+                    id="activity-notifications"
                     :icon="faBell"
                     :is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
                     title="Notifications"
                     @click="toggleSidebar('notifications')" />
                 <ActivityItem
-                    id="settings"
+                    id="activity-settings"
                     :activity-bar-id="props.activityBarId"
                     :icon="props.optionsIcon"
                     :is-active="isActiveSideBar('settings')"
@@ -280,7 +280,7 @@ defineExpose({
                     @click="toggleSidebar('settings')" />
                 <ActivityItem
                     v-if="isAdmin && showAdmin"
-                    id="admin"
+                    id="activity-admin"
                     :activity-bar-id="props.activityBarId"
                     :icon="faUserCog"
                     :is-active="isActiveSideBar('admin')"

From d4f1ceb1733aa162d7f13ea86c29a156b318cd54 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 20:50:32 +0100
Subject: [PATCH 110/131] fix test save as

---
 client/src/components/Workflow/Editor/modules/activities.ts | 2 +-
 client/src/utils/navigation/navigation.yml                  | 2 +-
 lib/galaxy_test/selenium/test_workflow_editor.py            | 5 +----
 3 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Workflow/Editor/modules/activities.ts b/client/src/components/Workflow/Editor/modules/activities.ts
index f85c012b2a48..aed37909dea2 100644
--- a/client/src/components/Workflow/Editor/modules/activities.ts
+++ b/client/src/components/Workflow/Editor/modules/activities.ts
@@ -92,7 +92,7 @@ export const workflowEditorActivities = [
         id: "save-workflow-as",
         title: "Save as",
         tooltip: "Save a copy of this workflow",
-        visible: false,
+        visible: true,
         click: true,
         optional: true,
     },
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 377de4e5c0b8..3571c7362af8 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -734,7 +734,6 @@ workflow_editor:
       duplicate_selection: "[title='duplicate selected']"
       delete_selection: "[title='delete selected']"
       auto_layout: "#auto-layout-button"
-      option_save_as: ".activity-settings-item[contains(text(), 'Save as')]"
   comment:
     selectors:
       _: ".workflow-editor-comment"
@@ -841,6 +840,7 @@ workflow_editor:
     state_modal_body: '.state-upgrade-modal'
     modal_button_continue: '.modal-footer .btn'
     workflow_activity: '#activity-workflow-editor-workflows'
+    save_as_activity: "#save-workflow-as"
 
 workflow_show:
   selectors:
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index d9997dbfa26e..d40d2f6d222f 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -472,10 +472,7 @@ def test_save_as(self):
         self.sleep_for(self.wait_types.UX_RENDER)
         self.screenshot("workflow_editor_edit_menu")
 
-        if self.element_absent(self.components.workflow_editor.tool_bar.option_save_as):
-            self.components.preferences.activity.wait_for_and_click()
-
-        self.components.workflow_editor.tool_bar.option_save_as.wait_for_and_click()
+        self.components.workflow_editor.save_as_activity.wait_for_and_click()
 
     @selenium_test
     def test_editor_tool_upgrade(self):

From 866b2df93cd55112732bebae6a275a4f811f3059 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 21:02:13 +0100
Subject: [PATCH 111/131] add more wait time for search

---
 lib/galaxy/selenium/navigates_galaxy.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index b3a5dceb7d5f..11584657ae0c 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1250,7 +1250,7 @@ def workflow_editor_search_for_workflow(self, name: str):
         input = self.wait_for_selector(".activity-panel input")
         input.send_keys(name)
 
-        self.sleep_for(self.wait_types.UX_RENDER)
+        self.sleep_for(self.wait_types.DATABASE_OPERATION)
 
     def workflow_editor_add_steps(self, name: str):
         self.workflow_editor_search_for_workflow(name)

From 80ac0c1da84a3eb341acec3fb843abe671ef67a3 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 21:22:42 +0100
Subject: [PATCH 112/131] re-add confirm dialog

---
 client/src/components/Workflow/Editor/Index.vue | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index 408282998b9b..fa9bc8df5a93 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -724,6 +724,20 @@ export default {
             this.showSaveAsModal = true;
         },
         async saveOrCreate() {
+            if (this.hasInvalidConnections) {
+                const confirmed = await confirm(
+                    `Workflow has invalid connections. You can save the workflow, but it may not run correctly.`,
+                    {
+                        id: "save-workflow-confirmation",
+                        okTitle: "Save Workflow",
+                    }
+                );
+
+                if (!confirmed) {
+                    return;
+                }
+            }
+
             if (this.isNewTempWorkflow) {
                 await this.onCreate();
             } else {

From 5855a2ff8938e940d04958ae169eb02febca05d2 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Wed, 20 Nov 2024 23:49:34 +0100
Subject: [PATCH 113/131] remove double id prepending

---
 client/src/components/ActivityBar/ActivityBar.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue
index a9d8151280b7..5371d14f84fe 100644
--- a/client/src/components/ActivityBar/ActivityBar.vue
+++ b/client/src/components/ActivityBar/ActivityBar.vue
@@ -265,13 +265,13 @@ defineExpose({
             <b-nav v-if="!isAnonymous" vertical class="activity-footer flex-nowrap p-1">
                 <NotificationItem
                     v-if="isConfigLoaded && config.enable_notification_system"
-                    id="activity-notifications"
+                    id="notifications"
                     :icon="faBell"
                     :is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
                     title="Notifications"
                     @click="toggleSidebar('notifications')" />
                 <ActivityItem
-                    id="activity-settings"
+                    id="settings"
                     :activity-bar-id="props.activityBarId"
                     :icon="props.optionsIcon"
                     :is-active="isActiveSideBar('settings')"
@@ -280,7 +280,7 @@ defineExpose({
                     @click="toggleSidebar('settings')" />
                 <ActivityItem
                     v-if="isAdmin && showAdmin"
-                    id="activity-admin"
+                    id="admin"
                     :activity-bar-id="props.activityBarId"
                     :icon="faUserCog"
                     :is-active="isActiveSideBar('admin')"

From 802c6da1f52304db6f8813adb1adad990fe71ebe Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 00:10:30 +0100
Subject: [PATCH 114/131] redo add subworkflow

---
 .../src/components/Workflow/List/WorkflowCard.vue  |  6 +++++-
 client/src/utils/navigation/navigation.yml         |  1 +
 lib/galaxy/selenium/navigates_galaxy.py            | 14 +++++++++-----
 3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue
index cc819e5dd63d..953ac58ee5aa 100644
--- a/client/src/components/Workflow/List/WorkflowCard.vue
+++ b/client/src/components/Workflow/List/WorkflowCard.vue
@@ -76,7 +76,11 @@ const dropdownOpen = ref(false);
 </script>
 
 <template>
-    <div class="workflow-card" :class="{ 'dropdown-open': dropdownOpen }" :data-workflow-id="workflow.id">
+    <div
+        class="workflow-card"
+        :class="{ 'dropdown-open': dropdownOpen }"
+        :data-workflow-id="workflow.id"
+        :data-workflow-name="workflow.name">
         <div
             class="workflow-card-container"
             :class="{
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 3571c7362af8..2851f090a5a7 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -608,6 +608,7 @@ workflows:
     workflows_list_empty: '#workflow-list-empty'
     workflow_not_found_message: '#no-workflow-found'
     workflow_card: '.workflow-card'
+    workflow_card_button: '.workflow-card[data-workflow-name="${name}"] button[title="${title}"]'
     workflow_cards: '#workflow-cards'
     external_link: '.workflow-external-link'
     trs_icon: '.workflow-trs-icon'
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 11584657ae0c..522caa779c46 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1250,21 +1250,25 @@ def workflow_editor_search_for_workflow(self, name: str):
         input = self.wait_for_selector(".activity-panel input")
         input.send_keys(name)
 
-        self.sleep_for(self.wait_types.DATABASE_OPERATION)
+        self.sleep_for(self.wait_types.UX_RENDER)
 
     def workflow_editor_add_steps(self, name: str):
         self.workflow_editor_search_for_workflow(name)
 
-        insert_button = self.wait_for_selector(".activity-panel button[title='Copy steps into workflow']")
-        insert_button.click()
+        insert_button = self.components.workflows.workflow_card_button(name=name, title="Copy steps into workflow")
+        insert_button.wait_for_and_click()
+
+        self.components.workflow_editor.node._(label=name).wait_for_present()
 
         self.sleep_for(self.wait_types.UX_RENDER)
 
     def workflow_editor_add_subworkflow(self, name: str):
         self.workflow_editor_search_for_workflow(name)
 
-        insert_button = self.wait_for_selector(".activity-panel button[title='Insert as sub-workflow']")
-        insert_button.click()
+        insert_button = self.components.workflows.workflow_card_button(name=name, title="Insert as sub-workflow")
+        insert_button.wait_for_and_click()
+
+        self.components.workflow_editor.node._(label=name).wait_for_present()
 
         self.sleep_for(self.wait_types.UX_RENDER)
 

From a472750e3edccf6f825e8c21492cea68fdbdbc58 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 00:12:08 +0100
Subject: [PATCH 115/131] fix save as selector

---
 client/src/utils/navigation/navigation.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 2851f090a5a7..6c146418a330 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -841,7 +841,7 @@ workflow_editor:
     state_modal_body: '.state-upgrade-modal'
     modal_button_continue: '.modal-footer .btn'
     workflow_activity: '#activity-workflow-editor-workflows'
-    save_as_activity: "#save-workflow-as"
+    save_as_activity: "#activity-save-workflow-as"
 
 workflow_show:
   selectors:

From 530be00481617260f5b952fe8cafea801ae8b7cc Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 00:21:37 +0100
Subject: [PATCH 116/131] move selection options to the left, so they do not
 interfere with the node inspector

---
 .../Workflow/Editor/Tools/ToolBar.vue         | 34 +++++++++----------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Tools/ToolBar.vue b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
index 0df88909f6fe..d68eaa62f0ce 100644
--- a/client/src/components/Workflow/Editor/Tools/ToolBar.vue
+++ b/client/src/components/Workflow/Editor/Tools/ToolBar.vue
@@ -272,6 +272,22 @@ function autoLayout() {
             </BButton>
         </div>
         <div v-if="toolbarVisible" class="options">
+            <div v-if="anySelected" class="selection-options">
+                <span>{{ selectedCountText }}</span>
+
+                <BButtonGroup>
+                    <BButton class="button" title="clear selection" @click="deselectAll">
+                        Clear <FontAwesomeIcon icon="fa-times" />
+                    </BButton>
+                    <BButton class="button" title="duplicate selected" @click="duplicateSelection">
+                        Duplicate <FontAwesomeIcon icon="fa-clone" />
+                    </BButton>
+                    <BButton class="button" title="delete selected" @click="deleteSelection">
+                        Delete <FontAwesomeIcon icon="fa-trash" />
+                    </BButton>
+                </BButtonGroup>
+            </div>
+
             <div
                 v-if="
                     toolbarStore.snapActive &&
@@ -398,22 +414,6 @@ function autoLayout() {
                 </BButtonGroup>
             </div>
         </div>
-
-        <div v-if="anySelected" class="selection-options">
-            <span>{{ selectedCountText }}</span>
-
-            <BButtonGroup>
-                <BButton class="button" title="clear selection" @click="deselectAll">
-                    Clear <FontAwesomeIcon icon="fa-times" />
-                </BButton>
-                <BButton class="button" title="duplicate selected" @click="duplicateSelection">
-                    Duplicate <FontAwesomeIcon icon="fa-clone" />
-                </BButton>
-                <BButton class="button" title="delete selected" @click="deleteSelection">
-                    Delete <FontAwesomeIcon icon="fa-trash" />
-                </BButton>
-            </BButtonGroup>
-        </div>
     </div>
 </template>
 
@@ -524,7 +524,7 @@ function autoLayout() {
         display: flex;
         padding: 0.25rem;
         gap: 0.25rem;
-        align-items: end;
+        align-items: start;
         flex-direction: column-reverse;
         align-self: flex-start;
 

From 7596e6aa2225320dbae11609c9f34b88642b2089 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 00:23:21 +0100
Subject: [PATCH 117/131] remove collapse buttons from basics test

---
 lib/galaxy_test/selenium/test_workflow_editor.py | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index d40d2f6d222f..da9969bbd814 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -50,18 +50,11 @@ def test_basics(self):
         self.screenshot("workflow_editor_blank")
 
         self.hover_over(self.components._.left_panel_drag.wait_for_visible())
-        self.components._.left_panel_collapse.wait_for_and_click()
-
-        self.sleep_for(self.wait_types.UX_RENDER)
-
-        self.screenshot("workflow_editor_left_collapsed")
-
         self.hover_over(self.components._.right_panel_drag.wait_for_visible())
-        self.components._.right_panel_collapse.wait_for_and_click()
 
-        self.sleep_for(self.wait_types.UX_RENDER)
+        self.workflow_editor_maximize_center_pane()
 
-        self.screenshot("workflow_editor_left_and_right_collapsed")
+        self.screenshot("workflow_editor_center_pane_maximized")
 
     @selenium_test
     def test_edit_annotation(self):

From 4f7dd00599f4d6eb6fb4f90735c50f3d1867dd46 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:22:58 +0100
Subject: [PATCH 118/131] wait for save to complete

---
 lib/galaxy_test/selenium/framework.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/galaxy_test/selenium/framework.py b/lib/galaxy_test/selenium/framework.py
index 9c300dc7b0de..d9cdb995a386 100644
--- a/lib/galaxy_test/selenium/framework.py
+++ b/lib/galaxy_test/selenium/framework.py
@@ -455,6 +455,7 @@ def assert_workflow_has_changes_and_save(self):
         save_button.wait_for_visible()
         assert not save_button.has_class("disabled")
         save_button.wait_for_and_click()
+        save_button.wait_for_absent()
         self.sleep_for(self.wait_types.UX_RENDER)
 
     @retry_assertion_during_transitions

From 6c3b2ce8d4a7747335fd09c808152bab9ebee1b4 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:34:08 +0100
Subject: [PATCH 119/131] remove test for removed feature

---
 .../selenium/test_workflow_editor.py          | 24 -------------------
 1 file changed, 24 deletions(-)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index da9969bbd814..6d8708ad3076 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -23,7 +23,6 @@
 )
 from .framework import (
     retry_assertion_during_transitions,
-    retry_during_transitions,
     RunsWorkflows,
     selenium_test,
     SeleniumTestCase,
@@ -878,29 +877,6 @@ def test_missing_tools(self):
         self.assert_modal_has_text("Tool is not installed")
         self.screenshot("workflow_editor_missing_tool")
 
-    @selenium_test
-    def test_workflow_bookmarking(self):
-        @retry_during_transitions
-        def assert_workflow_bookmarked_status(target_status):
-            name_matches = [c.text == new_workflow_name for c in self.components.tool_panel.workflow_names.all()]
-            status = any(name_matches)
-            assert status == target_status
-
-        new_workflow_name = self.workflow_create_new(clear_placeholder=True)
-        self.components.workflow_editor.canvas_body.wait_for_visible()
-        self.wait_for_selector_absent_or_hidden(self.modal_body_selector())
-
-        # Assert workflow not initially bookmarked.
-        self.navigate_to_tools()
-        assert_workflow_bookmarked_status(False)
-
-        self.click_activity_workflow()
-        self.components.workflows.bookmark_link(action="add").wait_for_and_click()
-
-        # search for bookmark in tools menu
-        self.navigate_to_tools()
-        assert_workflow_bookmarked_status(True)
-
     def tab_to(self, accessible_name, direction="forward"):
         for _ in range(100):
             ac = self.action_chains()

From 0873c1be17267205bc6a1e6d7d6b65aeedc56f47 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:35:13 +0100
Subject: [PATCH 120/131] remove invalid check in insert steps

---
 lib/galaxy/selenium/navigates_galaxy.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 522caa779c46..0ca2745ae32c 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1258,8 +1258,6 @@ def workflow_editor_add_steps(self, name: str):
         insert_button = self.components.workflows.workflow_card_button(name=name, title="Copy steps into workflow")
         insert_button.wait_for_and_click()
 
-        self.components.workflow_editor.node._(label=name).wait_for_present()
-
         self.sleep_for(self.wait_types.UX_RENDER)
 
     def workflow_editor_add_subworkflow(self, name: str):

From 61fef8695f0452a2623f922e9f475e7014a33f61 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:35:51 +0100
Subject: [PATCH 121/131] remove test for removed component

---
 lib/galaxy_test/selenium/test_workflow_editor.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 6d8708ad3076..6f784fb7f64d 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -49,7 +49,6 @@ def test_basics(self):
         self.screenshot("workflow_editor_blank")
 
         self.hover_over(self.components._.left_panel_drag.wait_for_visible())
-        self.hover_over(self.components._.right_panel_drag.wait_for_visible())
 
         self.workflow_editor_maximize_center_pane()
 

From 8b0d78bad7b287cce9d984d8b4529348b9b91fc6 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 11:33:05 +0100
Subject: [PATCH 122/131] make search selector more specific

---
 client/src/utils/navigation/navigation.yml | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 6c146418a330..b31d940df02a 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -809,10 +809,7 @@ workflow_editor:
       type: xpath
       selector: >
         //div[@data-label='Change datatype']//div[contains(@class, 'multiselect')]
-    select_datatype_text_search:
-      type: xpath
-      selector: >
-        //input[@class="multiselect__input"]
+    select_datatype_text_search: "div[data-label='Change datatype'] .multiselect__input"
     select_datatype:
       type: xpath
       selector: >

From c11cba1d8b7f136da1f2d41c1d5f939ebd276911 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 11:48:32 +0100
Subject: [PATCH 123/131] fix selection test

---
 lib/galaxy_test/selenium/test_workflow_editor.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 6f784fb7f64d..1cc294ce155d 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -1228,6 +1228,7 @@ def test_editor_selection(self):
         self.mouse_drag(from_element=tool_node, to_element=canvas, to_offset=(0, -100))
 
         # select the node
+        editor.node_inspector_close.wait_for_and_click()
         self.action_chains().move_to_element(tool_node).key_down(Keys.SHIFT).click().key_up(Keys.SHIFT).perform()
         self.sleep_for(self.wait_types.UX_RENDER)
 

From 477546e71f6c1e47215f21dd9c6da354e2043208 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 12:03:35 +0100
Subject: [PATCH 124/131] clear search before searching for other tool

---
 client/src/utils/navigation/navigation.yml | 1 +
 lib/galaxy/selenium/navigates_galaxy.py    | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index b31d940df02a..a6ec2636a79f 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -902,6 +902,7 @@ tools:
     execute: '#execute'
     help: 'div.form-help'
     search: '#toolbox-panel .search-query'
+    clear_search: '#toolbox-panel .search-clear'
     title: '.toolSectionTitle'
 
 admin:
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 0ca2745ae32c..04a9372723e6 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1685,6 +1685,9 @@ def open_toolbox(self):
     def tool_open(self, tool_id, outer=False):
         self.open_toolbox()
 
+        self.components.tools.clear_search.wait_for_and_click()
+        self.sleep_for(self.wait_types.UX_RENDER)
+
         self.components.tools.search.wait_for_and_send_keys(f"id:{tool_id}")
 
         if outer:

From 8979126e2826becb1fdeee507d6e0c94d932036d Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 17:44:42 +0100
Subject: [PATCH 125/131] use confirm dialog composable

---
 client/src/components/Workflow/Editor/Index.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index fa9bc8df5a93..9506a25386af 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -191,7 +191,7 @@ import { storeToRefs } from "pinia";
 import Vue, { computed, nextTick, onUnmounted, ref, unref, watch } from "vue";
 
 import { getUntypedWorkflowParameters } from "@/components/Workflow/Editor/modules/parameters";
-import { ConfirmDialog } from "@/composables/confirmDialog";
+import { ConfirmDialog, useConfirmDialog } from "@/composables/confirmDialog";
 import { useDatatypesMapper } from "@/composables/datatypesMapper";
 import { useMagicKeys } from "@/composables/useMagicKeys";
 import { useUid } from "@/composables/utils/uid";
@@ -465,6 +465,8 @@ export default {
             }))
         );
 
+        const { confirm } = useConfirmDialog();
+
         return {
             id,
             name,
@@ -507,6 +509,7 @@ export default {
             specialWorkflowActivities,
             isNewTempWorkflow,
             saveWorkflowTitle,
+            confirm,
         };
     },
     data() {
@@ -725,7 +728,7 @@ export default {
         },
         async saveOrCreate() {
             if (this.hasInvalidConnections) {
-                const confirmed = await confirm(
+                const confirmed = await this.confirm(
                     `Workflow has invalid connections. You can save the workflow, but it may not run correctly.`,
                     {
                         id: "save-workflow-confirmation",

From a7ea3796c7b98d9c99b1ae13b2544b4cf652988d Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 18:30:49 +0100
Subject: [PATCH 126/131] exclude node buttons from node selection targets

---
 .../src/components/Workflow/Editor/Node.vue   | 10 +++++-
 client/src/utils/dom.ts                       | 33 +++++++++++++++++++
 2 files changed, 42 insertions(+), 1 deletion(-)
 create mode 100644 client/src/utils/dom.ts

diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue
index ea9aa2ae19e3..1f4085242a70 100644
--- a/client/src/components/Workflow/Editor/Node.vue
+++ b/client/src/components/Workflow/Editor/Node.vue
@@ -165,6 +165,7 @@ import { useWorkflowStores } from "@/composables/workflowStores";
 import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore";
 import { useWorkflowNodeInspectorStore } from "@/stores/workflowNodeInspectorStore";
 import type { Step } from "@/stores/workflowStepStore";
+import { composedPartialPath, isClickable } from "@/utils/dom";
 
 import { ToggleStepSelectedAction } from "./Actions/stepActions";
 import type { OutputTerminals } from "./modules/terminals";
@@ -324,7 +325,14 @@ function onPointerDown() {
     mouseDownTime = Date.now();
 }
 
-function onPointerUp() {
+function onPointerUp(e: PointerEvent) {
+    const path = composedPartialPath(e);
+    const unclickable = path.every((target) => !isClickable(target as Element));
+
+    if (!unclickable) {
+        return;
+    }
+
     const mouseUpTime = Date.now();
     const clickTime = mouseUpTime - mouseDownTime;
 
diff --git a/client/src/utils/dom.ts b/client/src/utils/dom.ts
new file mode 100644
index 000000000000..7fd2ec12dd68
--- /dev/null
+++ b/client/src/utils/dom.ts
@@ -0,0 +1,33 @@
+/**
+ * composed array of all Event Targets between the original and current target
+ *
+ * @param e Event to return partial composed path of
+ */
+export function composedPartialPath(e: Event): EventTarget[] {
+    const current = e.currentTarget;
+    const composed = e.composedPath();
+
+    const currentIndex = composed.findIndex((target) => target === current);
+
+    if (currentIndex === -1) {
+        throw new Error("current target is not part of composed path");
+    }
+
+    const partial = composed.slice(0, currentIndex);
+
+    return partial;
+}
+
+/**
+ * checks if an element has a valid mouse interaction
+ */
+export function isClickable(element: Element): boolean {
+    const clickable =
+        element instanceof HTMLButtonElement ||
+        element instanceof HTMLInputElement ||
+        element instanceof HTMLAnchorElement ||
+        element instanceof HTMLSelectElement ||
+        element instanceof HTMLLabelElement;
+
+    return clickable;
+}

From 127893fef2da6201710a90e18e862fca653ab7db Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 18:51:42 +0100
Subject: [PATCH 127/131] use latest id on insert

---
 client/src/components/Workflow/List/WorkflowCardList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index dd368b76cb7a..ed6d8bb453af 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -60,7 +60,7 @@ function onPreview(id: string) {
 
 // TODO: clean-up types, as soon as better Workflow type is available
 function onInsert(workflow: Workflow) {
-    emit("insertWorkflow", workflow.id as any, workflow.name as any);
+    emit("insertWorkflow", workflow.latest_id as any, workflow.name as any);
 }
 
 function onInsertSteps(workflow: Workflow) {

From 8d26e19fec802b2c9a1003f69008e811dd28d1f3 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 19:09:59 +0100
Subject: [PATCH 128/131] save latest id to dict

---
 client/src/components/Workflow/List/WorkflowCardList.vue | 2 +-
 lib/galaxy/model/__init__.py                             | 2 ++
 lib/galaxy/webapps/galaxy/services/workflows.py          | 4 +++-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue
index ed6d8bb453af..a4ed88d3a193 100644
--- a/client/src/components/Workflow/List/WorkflowCardList.vue
+++ b/client/src/components/Workflow/List/WorkflowCardList.vue
@@ -60,7 +60,7 @@ function onPreview(id: string) {
 
 // TODO: clean-up types, as soon as better Workflow type is available
 function onInsert(workflow: Workflow) {
-    emit("insertWorkflow", workflow.latest_id as any, workflow.name as any);
+    emit("insertWorkflow", workflow.latest_workflow_id as any, workflow.name as any);
 }
 
 function onInsertSteps(workflow: Workflow) {
diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index a5e03373dfb7..bd5d59384efd 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -7766,6 +7766,7 @@ class StoredWorkflow(Base, HasTags, Dictifiable, RepresentById, UsesCreateAndUpd
 
     dict_collection_visible_keys = [
         "id",
+        "latest_workflow_id",
         "name",
         "create_time",
         "update_time",
@@ -7776,6 +7777,7 @@ class StoredWorkflow(Base, HasTags, Dictifiable, RepresentById, UsesCreateAndUpd
     ]
     dict_element_visible_keys = [
         "id",
+        "latest_workflow_id",
         "name",
         "create_time",
         "update_time",
diff --git a/lib/galaxy/webapps/galaxy/services/workflows.py b/lib/galaxy/webapps/galaxy/services/workflows.py
index 0f7f13b3aa05..f34ce0db46b2 100644
--- a/lib/galaxy/webapps/galaxy/services/workflows.py
+++ b/lib/galaxy/webapps/galaxy/services/workflows.py
@@ -70,7 +70,9 @@ def index(
         query, total_matches = self._workflows_manager.index_query(trans, payload, include_total_count)
         rval = []
         for wf in query.all():
-            item = wf.to_dict(value_mapper={"id": trans.security.encode_id})
+            item = wf.to_dict(
+                value_mapper={"id": trans.security.encode_id, "latest_workflow_id": trans.security.encode_id}
+            )
             encoded_id = trans.security.encode_id(wf.id)
             item["annotations"] = [x.annotation for x in wf.annotations]
             item["url"] = web.url_for("workflow", id=encoded_id)

From 8a2edfcb4cc8784a583b42a473a90bcd168d3eb4 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 23:37:58 +0100
Subject: [PATCH 129/131] fix test_non_data_connections test

---
 lib/galaxy_test/selenium/test_workflow_editor.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index 1cc294ce155d..ce5faa2c5093 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -257,6 +257,7 @@ def test_non_data_connections(self):
         editor = self.components.workflow_editor
 
         tool_node = editor.node._(label="tool_exec")
+        tool_node.wait_for_and_click()
         tool_input = tool_node.input_terminal(name="inttest")
         self.hover_over(tool_input.wait_for_visible())
         tool_node.connector_destroy_callout(name="inttest").wait_for_and_click()

From d894082fba5f87f686d91fecb4f33c0ebe727c61 Mon Sep 17 00:00:00 2001
From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
Date: Mon, 25 Nov 2024 23:51:37 +0100
Subject: [PATCH 130/131] fix test_editor_duplicate_node test

---
 client/src/utils/navigation/navigation.yml       | 1 +
 lib/galaxy_test/selenium/test_workflow_editor.py | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index a6ec2636a79f..6285837e44a8 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -703,6 +703,7 @@ workflow_editor:
   node:
     selectors:
       _: "[node-label='${label}']"
+      by_id: "#wf-node-step-${id}"
       title: '${_} .node-title'
       destroy: '${_} .node-destroy'
       clone: '${_} .node-clone'
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index ce5faa2c5093..f15d37846be2 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -694,6 +694,8 @@ def test_editor_duplicate_node(self):
         editor.remove_tags_input.wait_for_and_send_keys("#oldboringtag" + Keys.ENTER + Keys.ESCAPE)
         self.sleep_for(self.wait_types.UX_RENDER)
         cat_node.clone.wait_for_and_click()
+        cloned_node = editor.node.by_id(id=2)
+        cloned_node.wait_for_and_click()
         editor.label_input.wait_for_and_send_keys(Keys.BACKSPACE * 20)
         editor.label_input.wait_for_and_send_keys("cloned label")
         output_label = editor.label_output(output="out_file1")

From 3fc286dd396071bfdccb8da1b1e62c840850351b Mon Sep 17 00:00:00 2001
From: davelopez <46503462+davelopez@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:49:50 +0100
Subject: [PATCH 131/131] fix event name for markdown update in
 PageEditorMarkdown component

After refactoring `move MarkdownEditor to activity bar`
---
 client/src/components/PageEditor/PageEditorMarkdown.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/PageEditor/PageEditorMarkdown.vue b/client/src/components/PageEditor/PageEditorMarkdown.vue
index 6e1cb5bcc76a..3d4834f4cd1e 100644
--- a/client/src/components/PageEditor/PageEditorMarkdown.vue
+++ b/client/src/components/PageEditor/PageEditorMarkdown.vue
@@ -4,7 +4,7 @@
         :markdown-text="markdownText"
         :markdown-config="contentData"
         mode="page"
-        @onUpdate="onUpdate">
+        @update="onUpdate">
         <template v-slot:buttons>
             <ObjectPermissionsModal
                 id="object-permissions-modal"