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 fad7719f0e83693f706d9a2b89f90f82652d149c
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 22:43:40 2026 -0400

    feat(launcher): InputList + OutputList + FileOutputRow
---
 .../js/components/launch/FileOutputRow.vue         | 43 ++++++++++++++++++++
 .../js/components/launch/InputList.vue             | 33 ++++++++++++++++
 .../js/components/launch/OutputList.vue            | 25 ++++++++++++
 .../tests/unit/components/launch/InputList.spec.ts | 46 ++++++++++++++++++++++
 4 files changed, 147 insertions(+)

diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue
new file mode 100644
index 000000000..338add1f0
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue
@@ -0,0 +1,43 @@
+<template>
+  <div class="d-flex align-items-center gap-2 border rounded p-2 mb-1">
+    <div class="flex-shrink-0" style="width: 140px;">
+      <code>{{ descriptor.name }}</code>
+      <span class="text-muted small ms-1">{{ descriptor.type }}</span>
+    </div>
+    <select
+      class="form-select form-select-sm"
+      style="width: 160px;"
+      :value="modelValue?.storage_id ?? ''"
+      @change="onStorage(($event.target as HTMLSelectElement).value)"
+    >
+      <option value="" disabled>Storage…</option>
+      <option v-for="s in storages" :key="s.storage_id" 
:value="s.storage_id">{{ s.name }}</option>
+    </select>
+    <input
+      class="form-control form-control-sm flex-grow-1"
+      :data-test="`file-out-path-${descriptor.name}`"
+      :value="modelValue?.path ?? ''"
+      placeholder="/path/to/output"
+      @input="onPath(($event.target as HTMLInputElement).value)"
+    />
+    <span class="badge bg-info text-dark">stage-out</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { IODescriptor, StorageRef, UserStorage } from 
"django-airavata-common-ui/js/stores/launch-types";
+
+const props = defineProps<{
+  descriptor: IODescriptor;
+  modelValue: StorageRef | null;
+  storages: UserStorage[];
+}>();
+const emit = defineEmits<{ "update:modelValue": [v: StorageRef] }>();
+
+function onStorage(id: string) {
+  emit("update:modelValue", { storage_id: id, path: props.modelValue?.path ?? 
"" });
+}
+function onPath(p: string) {
+  emit("update:modelValue", { storage_id: props.modelValue?.storage_id ?? "", 
path: p });
+}
+</script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue
new file mode 100644
index 000000000..8768a604d
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue
@@ -0,0 +1,33 @@
+<template>
+  <div v-if="store.pickedInterface">
+    <div
+      v-for="io in store.pickedInterface.inputs"
+      :key="io.name"
+      data-test="input-row"
+    >
+      <FileInputRow
+        v-if="io.type === 'file' || io.type === 'dir'"
+        :descriptor="io"
+        :model-value="(store.draft.inputs[io.name] as { storage_id: string; 
path: string } | null) ?? null"
+        :storages="storages"
+        @update:model-value="store.setInput(io.name, $event)"
+      />
+      <ScalarInputRow
+        v-else
+        :descriptor="io"
+        :model-value="(store.draft.inputs[io.name] as string | number | 
boolean | null) ?? null"
+        @update:model-value="store.setInput(io.name, $event)"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { UserStorage } from 
"django-airavata-common-ui/js/stores/launch-types";
+import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+import FileInputRow from "./FileInputRow.vue";
+import ScalarInputRow from "./ScalarInputRow.vue";
+
+defineProps<{ storages: UserStorage[] }>();
+const store = useLaunchStore();
+</script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue
new file mode 100644
index 000000000..a74e31d28
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue
@@ -0,0 +1,25 @@
+<template>
+  <div v-if="store.pickedInterface">
+    <FileOutputRow
+      v-for="io in fileOutputs"
+      :key="io.name"
+      :descriptor="io"
+      :model-value="store.draft.outputs[io.name] ?? null"
+      :storages="storages"
+      @update:model-value="store.setOutput(io.name, $event)"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import type { UserStorage } from 
"django-airavata-common-ui/js/stores/launch-types";
+import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+import FileOutputRow from "./FileOutputRow.vue";
+
+defineProps<{ storages: UserStorage[] }>();
+const store = useLaunchStore();
+const fileOutputs = computed(() =>
+  (store.pickedInterface?.outputs ?? []).filter((o) => o.type === "file" || 
o.type === "dir"),
+);
+</script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts
new file mode 100644
index 000000000..2354fe1be
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts
@@ -0,0 +1,46 @@
+import { mount } from "@vue/test-utils";
+import { createPinia, setActivePinia } from "pinia";
+import { beforeEach, describe, expect, it } from "vitest";
+import InputList from "../../../../js/components/launch/InputList.vue";
+import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch";
+
+const APP = {
+  app_id: "namd", name: "NAMD", category: "MD",
+  content: { kind: "github" as const, url: "g" },
+  interfaces: [{
+    name: "run",
+    inputs: [
+      { name: "sim_dir", type: "dir" as const, required: true },
+      { name: "steps", type: "int" as const, required: true },
+    ],
+    outputs: [],
+  }],
+};
+
+const STORAGES = [{ storage_id: "my-home", name: "My Home", is_primary: true 
}];
+
+describe("InputList", () => {
+  beforeEach(() => {
+    setActivePinia(createPinia());
+    const s = useLaunchStore();
+    s.pickApp(APP);
+    s.pickInterface("run");
+  });
+
+  it("renders one row per declared input", () => {
+    const w = mount(InputList, { props: { storages: STORAGES } });
+    expect(w.findAll("[data-test='input-row']")).toHaveLength(2);
+  });
+
+  it("scalar input writes through to the store", async () => {
+    const w = mount(InputList, { props: { storages: STORAGES } });
+    await w.find("input[data-test='scalar-steps']").setValue("5000");
+    expect(useLaunchStore().draft.inputs.steps).toBe(5000);
+  });
+
+  it("file input writes through to the store", async () => {
+    const w = mount(InputList, { props: { storages: STORAGES } });
+    await w.find("input[data-test='file-path-sim_dir']").setValue("/data");
+    expect(useLaunchStore().draft.inputs.sim_dir).toEqual({ storage_id: "", 
path: "/data" });
+  });
+});

Reply via email to