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 2e5af0f95cc738b8f418dc97e19efca7ff178597
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 23:04:43 2026 -0400

    feat(launcher): localStorage draft persistence + hydrate()
---
 .../js/containers/LaunchContainer.vue              |  3 ++
 .../tests/unit/stores/launch.spec.ts               | 34 ++++++++++++++++++++++
 .../static/common/js/stores/launch.ts              | 27 ++++++++++++++++-
 airavata-django-portal/tooling/vitest-setup.ts     | 16 ++++++++++
 airavata-django-portal/tooling/vitest.config.js    |  1 +
 5 files changed, 80 insertions(+), 1 deletion(-)

diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/LaunchContainer.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/LaunchContainer.vue
index d9a1d0809..2904afa27 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/LaunchContainer.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/LaunchContainer.vue
@@ -18,6 +18,7 @@
 <script setup lang="ts">
 import { onMounted, ref } from "vue";
 import { launcherService } from 
"django-airavata-common-ui/js/services/launcherService";
+import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
 import ExperimentMetaHeader from 
"../components/launch/ExperimentMetaHeader.vue";
 import WizardTabs from "../components/launch/WizardTabs.vue";
 import Tab1ApplicationInputs from 
"../components/launch/Tab1ApplicationInputs.vue";
@@ -26,6 +27,7 @@ import Tab3ReviewLaunch from 
"../components/launch/Tab3ReviewLaunch.vue";
 
 const active = ref<1 | 2 | 3>(1);
 const projects = ref<Array<{ project_id: string; name: string }>>([]);
+const store = useLaunchStore();
 
 function onChangeTab(n: 1 | 2 | 3) {
   active.value = n;
@@ -35,6 +37,7 @@ function onChangeTab(n: 1 | 2 | 3) {
 }
 
 onMounted(async () => {
+  store.hydrate();
   const url = new URL(window.location.href);
   const t = Number(url.searchParams.get("tab"));
   if (t === 1 || t === 2 || t === 3) active.value = t as 1 | 2 | 3;
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
index b064e646f..ad073e075 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
@@ -102,3 +102,37 @@ describe("setters and reset", () => {
     expect(s.tab1Valid).toBe(true);
   });
 });
+
+describe("draft persistence", () => {
+  beforeEach(() => {
+    setActivePinia(createPinia());
+    localStorage.clear();
+  });
+
+  it("saves draft to localStorage on every change", () => {
+    const s = useLaunchStore();
+    s.setMeta({ name: "abc", project_id: "p1", description: "" });
+    const stored = localStorage.getItem("launch-draft");
+    expect(stored).not.toBeNull();
+    expect(JSON.parse(stored!).name).toBe("abc");
+  });
+
+  it("restores draft from localStorage on hydrate()", () => {
+    localStorage.setItem("launch-draft", JSON.stringify({
+      name: "restored", project_id: "p1", description: "",
+      app_id: null, interface_name: null, inputs: {}, outputs: {},
+      runtime: { compute_resource_id: null, partition: null,
+                 walltime: "01:00:00", nodes: 1, cpus_per_node: 1 },
+    }));
+    const s = useLaunchStore();
+    s.hydrate();
+    expect(s.draft.name).toBe("restored");
+  });
+
+  it("reset() clears localStorage", () => {
+    const s = useLaunchStore();
+    s.setMeta({ name: "abc", project_id: "p1", description: "" });
+    s.reset();
+    expect(localStorage.getItem("launch-draft")).toBeNull();
+  });
+});
diff --git 
a/airavata-django-portal/django_airavata/static/common/js/stores/launch.ts 
b/airavata-django-portal/django_airavata/static/common/js/stores/launch.ts
index 5e34c91b8..d769e0017 100644
--- a/airavata-django-portal/django_airavata/static/common/js/stores/launch.ts
+++ b/airavata-django-portal/django_airavata/static/common/js/stores/launch.ts
@@ -1,5 +1,5 @@
 import { defineStore } from "pinia";
-import { computed, reactive, ref } from "vue";
+import { computed, reactive, ref, watch } from "vue";
 import type {
   Application,
   ExperimentDraft,
@@ -142,6 +142,29 @@ export const useLaunchStore = defineStore("launch", () => {
     profile.value = p;
   }
 
+  const STORAGE_KEY = "launch-draft";
+
+  function persist(draftValue: ExperimentDraft) {
+    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(draftValue)); } 
catch { /* ignore quota */ }
+  }
+
+  function clearPersisted() {
+    try { localStorage.removeItem(STORAGE_KEY); } catch { /* noop */ }
+  }
+
+  function hydrate() {
+    try {
+      const raw = localStorage.getItem(STORAGE_KEY);
+      if (!raw) return;
+      const parsed = JSON.parse(raw) as Partial<ExperimentDraft>;
+      Object.assign(draft, makeDraft(), parsed);
+    } catch {
+      /* ignore corrupted draft */
+    }
+  }
+
+  watch(draft, (d) => persist(d), { deep: true, flush: "sync" });
+
   function reset() {
     Object.assign(draft, makeDraft());
     pickedApp.value = null;
@@ -150,6 +173,7 @@ export const useLaunchStore = defineStore("launch", () => {
     previewError.value = null;
     previewLoading.value = false;
     lastPreviewedHash.value = null;
+    clearPersisted();
   }
 
   return {
@@ -173,6 +197,7 @@ export const useLaunchStore = defineStore("launch", () => {
     setRuntime,
     setStorages,
     setProfile,
+    hydrate,
     reset,
   };
 });
diff --git a/airavata-django-portal/tooling/vitest-setup.ts 
b/airavata-django-portal/tooling/vitest-setup.ts
index 4b4e4ece3..a553a6d4a 100644
--- a/airavata-django-portal/tooling/vitest-setup.ts
+++ b/airavata-django-portal/tooling/vitest-setup.ts
@@ -17,3 +17,19 @@ if (typeof globalThis.ResizeObserver === "undefined") {
     disconnect() {}
   };
 }
+
+// Node 25 ships a built-in `localStorage` stub that lacks the full Storage API
+// (e.g. `.clear()` is missing).  Replace it with a simple in-memory
+// implementation so tests that rely on localStorage work correctly in jsdom.
+(function installLocalStorage() {
+  const store: Record<string, string> = {};
+  const impl = {
+    get length() { return Object.keys(store).length; },
+    getItem(k: string) { return Object.prototype.hasOwnProperty.call(store, k) 
? store[k] : null; },
+    setItem(k: string, v: string) { store[k] = String(v); },
+    removeItem(k: string) { delete store[k]; },
+    clear() { for (const k of Object.keys(store)) delete store[k]; },
+    key(n: number) { return Object.keys(store)[n] ?? null; },
+  };
+  Object.defineProperty(globalThis, "localStorage", { value: impl, writable: 
true, configurable: true });
+})();
diff --git a/airavata-django-portal/tooling/vitest.config.js 
b/airavata-django-portal/tooling/vitest.config.js
index c2dab779e..04a6e7d7e 100644
--- a/airavata-django-portal/tooling/vitest.config.js
+++ b/airavata-django-portal/tooling/vitest.config.js
@@ -19,6 +19,7 @@ export function defineVitestConfig({ srcDir, overrides = {} 
}) {
     test: {
       globals: true,
       environment: "jsdom",
+      environmentOptions: { jsdom: { url: "http://localhost/"; } },
       setupFiles: [resolve(import.meta.dirname, "./vitest-setup.ts")],
       include: ["**/*.{test,spec}.{js,ts,mjs}", 
"**/tests/**/*.{test,spec}.{js,ts}"],
       exclude: ["**/node_modules/**", "**/dist/**", "**/tests/e2e/**"],

Reply via email to