This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch feat/generic-experiment-launcher
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git

commit e80fff597ece8c59972f1a725f26373b9c578343
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 23:06:56 2026 -0400

    feat(launcher): confirm prompts on app/interface/project change
---
 .../js/components/launch/AppPicker.vue                | 12 +++++++++++-
 .../js/components/launch/ExperimentMetaHeader.vue     | 17 ++++++++++++++++-
 .../js/components/launch/InterfacePicker.vue          | 16 +++++++++++++++-
 .../js/composables/useConfirmReset.ts                 |  5 +++++
 .../tests/unit/composables/useConfirmReset.spec.ts    | 19 +++++++++++++++++++
 5 files changed, 66 insertions(+), 3 deletions(-)

diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue
index 7df9ef1ad..b14af9bf0 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue
@@ -46,6 +46,7 @@
 import { computed, ref } from "vue";
 import type { Application } from 
"django-airavata-common-ui/js/stores/launch-types";
 import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+import { useConfirmReset } from "../../composables/useConfirmReset";
 
 const props = defineProps<{ applications: Application[] }>();
 const store = useLaunchStore();
@@ -74,7 +75,16 @@ const filtered = computed(() => {
   return xs;
 });
 
+const guardedPick = useConfirmReset(
+  "Switching app clears interface, inputs, and outputs. Continue?",
+  (a: Application) => store.pickApp(a),
+);
+
 function pick(a: Application) {
-  store.pickApp(a);
+  if (store.draft.app_id && store.draft.app_id !== a.app_id) {
+    guardedPick(a);
+  } else {
+    store.pickApp(a);
+  }
 }
 </script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
index 52fddd29b..2e9e030e2 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
@@ -38,6 +38,7 @@
 
 <script setup lang="ts">
 import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+import { useConfirmReset } from "../../composables/useConfirmReset";
 
 defineProps<{ projects: Array<{ project_id: string; name: string }> }>();
 
@@ -46,9 +47,23 @@ const store = useLaunchStore();
 function onName(v: string) {
   store.setMeta({ name: v, project_id: store.draft.project_id, description: 
store.draft.description });
 }
+
+const guardedProject = useConfirmReset(
+  "Switching project clears your runtime selections. Continue?",
+  (id: string | null) => {
+    store.setMeta({ name: store.draft.name, project_id: id, description: 
store.draft.description });
+  },
+);
+
 function onProject(v: string) {
-  store.setMeta({ name: store.draft.name, project_id: v || null, description: 
store.draft.description });
+  const next = v || null;
+  if (store.draft.project_id && store.draft.project_id !== next) {
+    guardedProject(next);
+  } else {
+    store.setMeta({ name: store.draft.name, project_id: next, description: 
store.draft.description });
+  }
 }
+
 function onDescription(v: string) {
   store.setMeta({ name: store.draft.name, project_id: store.draft.project_id, 
description: v });
 }
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InterfacePicker.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InterfacePicker.vue
index 5216b013a..e56aa94ac 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InterfacePicker.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InterfacePicker.vue
@@ -11,7 +11,7 @@
         class="card w-100 text-start p-2"
         :class="{ 'border-primary': store.draft.interface_name === iface.name 
}"
         :data-test="`iface-card-${iface.name}`"
-        @click="store.pickInterface(iface.name)"
+        @click="onPick(iface.name)"
       >
         <code class="d-block fw-bold">{{ iface.name }}</code>
         <small class="text-muted" :data-test="`iface-sig-${iface.name}`">
@@ -25,10 +25,24 @@
 <script setup lang="ts">
 import type { IODescriptor } from 
"django-airavata-common-ui/js/stores/launch-types";
 import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+import { useConfirmReset } from "../../composables/useConfirmReset";
 
 const store = useLaunchStore();
 
 function formatList(io: IODescriptor[]): string {
   return io.map((x) => `${x.name}: ${x.type}`).join(", ");
 }
+
+const guarded = useConfirmReset(
+  "Switching interface clears inputs and outputs. Continue?",
+  (n: string) => store.pickInterface(n),
+);
+
+function onPick(n: string) {
+  if (store.draft.interface_name && store.draft.interface_name !== n) {
+    guarded(n);
+  } else {
+    store.pickInterface(n);
+  }
+}
 </script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/composables/useConfirmReset.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/composables/useConfirmReset.ts
new file mode 100644
index 000000000..0f0285e66
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/composables/useConfirmReset.ts
@@ -0,0 +1,5 @@
+export function useConfirmReset<TArgs extends unknown[]>(message: string, fn: 
(...a: TArgs) => void) {
+  return (...args: TArgs) => {
+    if (window.confirm(message)) fn(...args);
+  };
+}
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/composables/useConfirmReset.spec.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/composables/useConfirmReset.spec.ts
new file mode 100644
index 000000000..a462cce2c
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/composables/useConfirmReset.spec.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it, vi } from "vitest";
+import { useConfirmReset } from "../../../js/composables/useConfirmReset";
+
+describe("useConfirmReset", () => {
+  it("calls onConfirm when window.confirm returns true", () => {
+    vi.spyOn(window, "confirm").mockReturnValueOnce(true);
+    const fn = vi.fn();
+    const guarded = useConfirmReset("ok?", fn);
+    guarded();
+    expect(fn).toHaveBeenCalledTimes(1);
+  });
+
+  it("skips onConfirm when window.confirm returns false", () => {
+    vi.spyOn(window, "confirm").mockReturnValueOnce(false);
+    const fn = vi.fn();
+    useConfirmReset("ok?", fn)();
+    expect(fn).not.toHaveBeenCalled();
+  });
+});

Reply via email to