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 1d1e486f72328b84a5896431e77b8cf3435dc58f
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 22:41:57 2026 -0400

    feat(launcher): ScalarInputRow + FileInputRow
---
 .../js/components/launch/FileInputRow.vue          | 43 +++++++++++++++++
 .../js/components/launch/ScalarInputRow.vue        | 56 ++++++++++++++++++++++
 .../unit/components/launch/FileInputRow.spec.ts    | 45 +++++++++++++++++
 .../unit/components/launch/ScalarInputRow.spec.ts  | 31 ++++++++++++
 4 files changed, 175 insertions(+)

diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.vue
new file mode 100644
index 000000000..df78b521d
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.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-path-${descriptor.name}`"
+      :value="modelValue?.path ?? ''"
+      placeholder="/path/to/data"
+      @input="onPath(($event.target as HTMLInputElement).value)"
+    />
+    <span data-test="io-badge" class="badge bg-warning 
text-dark">stage-in</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/ScalarInputRow.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ScalarInputRow.vue
new file mode 100644
index 000000000..6b33e2f90
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ScalarInputRow.vue
@@ -0,0 +1,56 @@
+<template>
+  <div class="row mb-1 align-items-center">
+    <label class="col-md-3 col-form-label" :for="`scalar-${descriptor.name}`">
+      <code>{{ descriptor.name }}</code>
+      <span class="text-muted small ms-1">{{ descriptor.type }}</span>
+    </label>
+    <div class="col-md-9">
+      <select
+        v-if="descriptor.type === 'enum'"
+        :id="`scalar-${descriptor.name}`"
+        :data-test="`scalar-${descriptor.name}`"
+        class="form-select"
+        :value="modelValue ?? ''"
+        @change="emit('update:modelValue', ($event.target as 
HTMLSelectElement).value)"
+      >
+        <option value="" disabled>Choose…</option>
+        <option v-for="o in descriptor.options ?? []" :key="o" :value="o">{{ o 
}}</option>
+      </select>
+      <input
+        v-else
+        :id="`scalar-${descriptor.name}`"
+        :data-test="`scalar-${descriptor.name}`"
+        class="form-control"
+        :type="inputType"
+        :value="modelValue ?? ''"
+        @input="onInput(($event.target as HTMLInputElement).value)"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import type { IODescriptor, ScalarValue } from 
"django-airavata-common-ui/js/stores/launch-types";
+
+const props = defineProps<{ descriptor: IODescriptor; modelValue: ScalarValue 
| null }>();
+const emit = defineEmits<{ "update:modelValue": [v: ScalarValue | null] }>();
+
+const inputType = computed(() => {
+  switch (props.descriptor.type) {
+    case "int":
+    case "float":
+      return "number";
+    case "bool":
+      return "checkbox";
+    default:
+      return "text";
+  }
+});
+
+function onInput(raw: string) {
+  if (props.descriptor.type === "int") emit("update:modelValue", raw === "" ? 
null : Number.parseInt(raw, 10));
+  else if (props.descriptor.type === "float") emit("update:modelValue", raw 
=== "" ? null : Number.parseFloat(raw));
+  else emit("update:modelValue", raw);
+}
+</script>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts
new file mode 100644
index 000000000..c16ab713c
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts
@@ -0,0 +1,45 @@
+import { mount } from "@vue/test-utils";
+import { describe, expect, it } from "vitest";
+import FileInputRow from "../../../../js/components/launch/FileInputRow.vue";
+
+const STORAGES = [
+  { storage_id: "my-home", name: "My Home", is_primary: true },
+  { storage_id: "scratch", name: "Scratch", is_primary: false },
+];
+
+describe("FileInputRow", () => {
+  it("emits update:modelValue when storage changes", async () => {
+    const w = mount(FileInputRow, {
+      props: {
+        descriptor: { name: "sim_dir", type: "dir", required: true },
+        modelValue: { storage_id: "my-home", path: "" },
+        storages: STORAGES,
+      },
+    });
+    await w.find("select").setValue("scratch");
+    expect(w.emitted("update:modelValue")?.at(-1)).toEqual([{ storage_id: 
"scratch", path: "" }]);
+  });
+
+  it("emits update:modelValue when path changes", async () => {
+    const w = mount(FileInputRow, {
+      props: {
+        descriptor: { name: "sim_dir", type: "dir", required: true },
+        modelValue: { storage_id: "my-home", path: "" },
+        storages: STORAGES,
+      },
+    });
+    await 
w.find("input[data-test='file-path-sim_dir']").setValue("/home/x/sim");
+    expect(w.emitted("update:modelValue")?.at(-1)).toEqual([{ storage_id: 
"my-home", path: "/home/x/sim" }]);
+  });
+
+  it("renders a stage-in badge", () => {
+    const w = mount(FileInputRow, {
+      props: {
+        descriptor: { name: "sim_dir", type: "dir", required: true },
+        modelValue: null,
+        storages: STORAGES,
+      },
+    });
+    expect(w.find("[data-test='io-badge']").text()).toMatch(/stage-in/i);
+  });
+});
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts
new file mode 100644
index 000000000..2d5c92bd6
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts
@@ -0,0 +1,31 @@
+import { mount } from "@vue/test-utils";
+import { describe, expect, it } from "vitest";
+import ScalarInputRow from 
"../../../../js/components/launch/ScalarInputRow.vue";
+
+describe("ScalarInputRow", () => {
+  it("renders a number input for type=int", async () => {
+    const w = mount(ScalarInputRow, {
+      props: { descriptor: { name: "steps", type: "int", required: true }, 
modelValue: null },
+    });
+    const input = w.find("input[data-test='scalar-steps']");
+    expect(input.attributes("type")).toBe("number");
+  });
+
+  it("emits update:modelValue on input", async () => {
+    const w = mount(ScalarInputRow, {
+      props: { descriptor: { name: "steps", type: "int", required: true }, 
modelValue: null },
+    });
+    await w.find("input").setValue("42");
+    expect(w.emitted("update:modelValue")?.at(-1)).toEqual([42]);
+  });
+
+  it("renders a select for enum descriptors", () => {
+    const w = mount(ScalarInputRow, {
+      props: {
+        descriptor: { name: "mode", type: "enum", required: true, options: 
["a", "b"] },
+        modelValue: null,
+      },
+    });
+    expect(w.find("select").exists()).toBe(true);
+  });
+});

Reply via email to