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(); + }); +});
