<template>
<GnModal
:open="open"
:title="`Run: ${script?.name ?? script?.alias ?? ''}`"
@update:open="$emit('update:open', $event)"
>
<div v-if="script?.params_schema" class="script-run-form">
<div
v-for="(config, name) in script.params_schema"
:key="name"
class="form-field"
>
<GnInput
v-if="config.type === 'text'"
v-model="formValues[name]"
:label="config.label || name"
:placeholder="config.placeholder || ''"
:state="fieldState(name, config)"
/>
<GnInput
v-else-if="config.type === 'number'"
v-model.number="formValues[name]"
type="number"
:label="config.label || name"
:placeholder="config.placeholder || ''"
:state="fieldState(name, config)"
/>
<GnRange
v-else-if="config.type === 'range'"
v-model.number="formValues[name]"
:label="config.label || name"
:min="config.min ?? 0"
:max="config.max ?? 100"
:step="config.step ?? 1"
:state="fieldState(name, config)"
/>
<GnSelect
v-else-if="config.type === 'select'"
v-model="formValues[name]"
:label="config.label || name"
:options="selectOptions(config.options)"
:state="fieldState(name, config)"
/>
<GnSwitch
v-else-if="config.type === 'toggle'"
v-model="formValues[name]"
:label="config.label || name"
/>
<GnTextarea
v-else-if="config.type === 'textarea'"
v-model="formValues[name]"
:label="config.label || name"
:placeholder="config.placeholder || ''"
:state="fieldState(name, config)"
/>
<p v-if="fieldError(name, config)" class="field-error">{{ fieldError(name, config) }}</p>
</div>
</div>
<template #footer>
<GnButton variant="secondary" @click="$emit('update:open', false)">
Cancel
</GnButton>
<GnButton
variant="primary"
icon="ph-play"
:disabled="!canSubmit"
@click="submit"
>
Run
</GnButton>
</template>
</GnModal>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import {
GnModal,
GnInput,
GnRange,
GnSelect,
GnSwitch,
GnTextarea,
GnButton,
} from "gnexus-ui-kit/vue";
const props = defineProps({
open: {
type: Boolean,
default: false,
},
script: {
type: Object,
default: null,
},
});
const emit = defineEmits(["update:open", "run"]);
const formValues = ref({});
const touched = ref(new Set());
function emptyValue(type, config) {
switch (type) {
case "text":
case "textarea":
return "";
case "number":
return 0;
case "range":
return config.min ?? 0;
case "select": {
const keys = Object.keys(config.options || {});
return keys.length ? keys[0] : "";
}
case "toggle":
return false;
default:
return "";
}
}
function resetForm() {
touched.value = new Set();
if (!props.script?.params_schema) {
formValues.value = {};
return;
}
const defaults = {};
for (const [name, config] of Object.entries(props.script.params_schema)) {
defaults[name] =
config.default !== undefined ? config.default : emptyValue(config.type, config);
}
formValues.value = defaults;
}
watch(
() => props.open,
(isOpen) => {
if (isOpen) {
resetForm();
}
},
{ immediate: true }
);
function selectOptions(options) {
if (!options) return [];
return Object.entries(options).map(([value, label]) => ({ value, label }));
}
function isFieldEmpty(name, config) {
const val = formValues.value[name];
if (val === undefined || val === null) return true;
if (config.type === "text" || config.type === "textarea") {
return String(val).trim() === "";
}
return false;
}
function fieldState(name, config) {
if (!touched.value.has(name)) return null;
if (config.required && isFieldEmpty(name, config)) return "error";
return null;
}
function fieldError(name, config) {
if (!touched.value.has(name)) return null;
if (config.required && isFieldEmpty(name, config)) {
return `${config.label || name} is required`;
}
return null;
}
const canSubmit = computed(() => {
if (!props.script?.params_schema) return true;
for (const [name, config] of Object.entries(props.script.params_schema)) {
if (config.required && isFieldEmpty(name, config)) {
return false;
}
}
return true;
});
function submit() {
// Touch all fields to show validation
for (const name of Object.keys(props.script?.params_schema || {})) {
touched.value.add(name);
}
if (!canSubmit.value) return;
emit("run", {
alias: props.script.alias,
params: { ...formValues.value },
});
emit("update:open", false);
}
</script>
<style scoped>
.script-run-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field-error {
color: var(--color-danger, #ef4444);
font-size: 12px;
margin: 0;
}
</style>