<script setup lang="ts"> import { useNameStore } from "@/stores/names"; import { useWorkflowStore } from "@/stores/workflows"; import { computed, onMounted, reactive, ref, watch } from "vue"; import { DocumentationEnum, type ParameterExtension } from "@/client/workflow"; import type { ClowmInfo } from "@/types/ClowmInfo"; import ParameterInput from "@/components/parameter-schema/form-mode/ParameterInput.vue"; import BootstrapToast from "@/components/BootstrapToast.vue"; import { Toast } from "bootstrap"; import DeleteModal from "@/components/modals/DeleteModal.vue"; // Props // ============================================================================= const props = defineProps<{ versionId: string; workflowId: string; }>(); // HTML refs // ============================================================================= const parameterExtensionForm = ref<HTMLFormElement | null>(null); let successToast: Toast | null = null; let deleteToast: Toast | null = null; // Repositories // ============================================================================= const nameRepository = useNameStore(); const workflowRepository = useWorkflowStore(); // Reactive State // ============================================================================= const parameterState = reactive<{ loading: boolean; makingRequest: boolean; extension: ParameterExtension; resourceParametersDefault: Set<string>; resourceParametersMapping: Set<string>; mappingParameterValues: Record<string, string>; formValidated: boolean; }>({ loading: true, makingRequest: false, extension: {}, resourceParametersDefault: new Set(), resourceParametersMapping: new Set(), mappingParameterValues: {}, formValidated: false, }); const parameterPools = reactive<{ defaults: string[]; mapping: string[]; }>({ defaults: [], mapping: [], }); // Watchers // ============================================================================= watch( () => workflowRepository.documentationFiles[props.versionId]?.parameter_schema, (newVal, old) => { if (newVal != old && newVal) { updateParameterPools(newVal); } }, ); watch( () => workflowRepository.documentationFiles[props.versionId]?.clowm_info, (newVal, old) => { if (newVal != old && newVal) { updateResourceParameters(newVal); } }, ); // Computed States // ============================================================================= // eslint-disable-next-line @typescript-eslint/no-explicit-any const parameterSchema = computed<Record<string, Record<string, any>>>(() => { const schema = workflowRepository.documentationFiles[props.versionId ?? ""] ?.parameter_schema; const a = schema?.["properties"] ?? {}; for (const group in schema?.["definitions"] ?? {}) { for (const param in schema?.["definitions"]?.[group]?.["properties"] ?? {}) { a[param] = schema["definitions"][group]["properties"][param]; } } return a; }); // Functions // ============================================================================= // eslint-disable-next-line @typescript-eslint/no-explicit-any function updateParameterPools(newVal?: object) { if (newVal) { const parameters = extractParameterList(newVal); parameterPools.defaults = parameters.slice(); parameterPools.mapping = parameters.filter( (param) => parameterSchema.value?.[param]?.["type"] !== "boolean" && !parameterSchema.value?.[param]?.["enum"], ); } if ( workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension ) { parameterPools.defaults = parameterPools.defaults.filter( (param) => workflowRepository.ownVersionMapping[props.versionId] ?.parameter_extension?.defaults?.[param] == undefined, ); parameterPools.mapping = parameterPools.mapping.filter( (param) => workflowRepository.ownVersionMapping[props.versionId] ?.parameter_extension?.defaults?.[param] == undefined, ); } } function updateResourceParameters(newVal?: ClowmInfo) { newVal?.resourceParameters?.forEach((param) => { parameterState.resourceParametersDefault.add(param); parameterState.resourceParametersMapping.add(param); }); } function submitForm() { if (parameterState.extension?.mapping) { for (const key of Object.keys(parameterState.extension.mapping)) { if (Object.keys(parameterState.extension?.mapping[key]).length === 0) { delete parameterState.extension?.mapping[key]; parameterPools.mapping.push(key); } } } parameterState.formValidated = true; if (parameterExtensionForm.value?.checkValidity()) { parameterState.makingRequest = true; workflowRepository .updateWorkflowExtension( props.workflowId, props.versionId, parameterState.extension, ) .then(() => { successToast?.show(); }) .finally(() => { parameterState.makingRequest = false; }); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractParameterList(schema: Record<string, any>): string[] { const groupedParameters = Object.keys(schema["definitions"] ?? {}).reduce( (acc: string[], val) => [ ...acc, ...Object.keys(schema["definitions"][val]["properties"]), ], [], ); const singleParameters = Object.keys(schema["properties"] ?? {}); return [...groupedParameters, ...singleParameters]; } function getParameterType(param: string): string | undefined { return parameterSchema.value[param]?.["type"]; } function getParameterSchemaDefault( param: string, ): string | boolean | number | undefined { return parameterSchema.value[param]?.["default"]; } function getParamDefault(param: string): string | boolean | number { switch (getParameterType(param)) { case "integer": { return getParameterSchemaDefault(param) ?? 0; } case "number": { return getParameterSchemaDefault(param) ?? 0; } case "boolean": { return getParameterSchemaDefault(param) ?? true; } case "string": { if (parameterState.resourceParametersDefault.has(param)) { return ""; } return ( getParameterSchemaDefault(param) ?? parameterSchema.value[param]?.["enum"]?.[0] ?? "" ); } default: { return ""; } } } function addDefaultParameter(param: string, index: number) { parameterState.formValidated = false; if (parameterState.extension.defaults == undefined) { parameterState.extension.defaults = {}; } parameterPools.defaults.splice(index, 1); parameterState.extension.defaults[param] = getParamDefault(param); } function makeResourceParameterDefault(param: string) { parameterState.formValidated = false; parameterState.resourceParametersDefault.add(param); parameterState.extension.defaults![param] = ""; } function makeResourceParameterMapping(param: string, val: string) { if (parameterState.extension.mapping?.[param]?.[val]) { parameterState.formValidated = false; parameterState.resourceParametersMapping.add(param); parameterState.extension.mapping[param][val] = ""; } } function deleteDefaultParameter(param: string) { if ( !workflowRepository.documentationFiles[ props.versionId ]?.clowm_info?.resourceParameters?.includes(param) ) { parameterState.resourceParametersDefault.delete(param); } delete parameterState.extension.defaults?.[param]; parameterPools.defaults.push(param); if (Object.keys(parameterState.extension.defaults ?? {}).length === 0) { parameterState.extension.defaults = undefined; } } function addMappingParameter(param: string, index: number) { parameterState.formValidated = false; if (parameterState.extension.mapping == undefined) { parameterState.extension.mapping = {}; } if (parameterState.extension.mapping[param] == undefined) { parameterState.extension.mapping[param] = {}; } parameterState.mappingParameterValues[param] = ""; if (index > -1) { parameterPools.mapping.splice(index, 1); } } function addMappingParameterValue(param: string, val: string) { parameterState.formValidated = false; if (parameterState.extension.mapping?.[param] != undefined) { parameterState.extension.mapping[param][val] = getParamDefault(param) as | string | number; parameterState.mappingParameterValues[param] = ""; } } function deleteMappingParameterValue(param: string, val: string) { if (parameterState.extension.mapping?.[param]?.[val] != undefined) { delete parameterState.extension.mapping[param][val]; } } function deleteMappingParameter(param: string) { if ( !workflowRepository.documentationFiles[ props.versionId ]?.clowm_info?.resourceParameters?.includes(param) ) { parameterState.resourceParametersMapping.delete(param); } delete parameterState.extension.mapping?.[param]; delete parameterState.mappingParameterValues[param]; parameterPools.mapping.push(param); if (Object.keys(parameterState.extension.mapping ?? {}).length === 0) { parameterState.extension.mapping = undefined; } } function deleteParameterExtension() { parameterState.makingRequest = true; workflowRepository .deleteWorkflowExtension(props.workflowId, props.versionId) .then(() => { parameterState.extension = {}; updateParameterPools( workflowRepository.documentationFiles[props.versionId] ?.parameter_schema, ); updateResourceParameters( workflowRepository.documentationFiles[props.versionId]?.clowm_info, ); deleteToast?.show(); }) .finally(() => { parameterState.makingRequest = false; }); } // Lifecycle Events // ============================================================================= onMounted(() => { successToast = new Toast("#save-parameter-extension-success-toast"); deleteToast = new Toast("#delete-parameter-extension-success-toast"); workflowRepository.fetchWorkflow(props.workflowId, true, () => { parameterState.extension = workflowRepository.ownVersionMapping[props.versionId] ?.parameter_extension ?? {}; for (const param of Object.keys(parameterState.extension?.mapping ?? {})) { for (const paramOption of Object.keys( parameterState.extension?.mapping?.[param] ?? {}, )) { if ( typeof parameterState.extension?.mapping?.[param]?.[paramOption] === "object" ) { parameterState.resourceParametersMapping.add(param); } } } workflowRepository .fetchWorkflowDocumentation( props.workflowId, props.versionId, DocumentationEnum.PARAMETER_SCHEMA, workflowRepository.ownVersionMapping[props.versionId]?.modes?.[0], ) .then(() => workflowRepository.fetchWorkflowDocumentation( props.workflowId, props.versionId, DocumentationEnum.CLOWM_INFO, workflowRepository.ownVersionMapping[props.versionId]?.modes?.[0], ), ) .finally(() => { parameterState.loading = false; updateParameterPools( workflowRepository.documentationFiles[props.versionId] ?.parameter_schema, ); updateResourceParameters( workflowRepository.documentationFiles[props.versionId]?.clowm_info, ); }); }); }); </script> <template> <bootstrap-toast toast-id="save-parameter-extension-success-toast" color-class="success" > <template #default>Successfully saved Parameter Extension</template> <template #body> <div class="d-grid gap-2"> <router-link class="btn btn-info btn-sm" role="button" :to="{ name: 'workflow-start', params: { versionId: props.versionId, workflowId: props.workflowId, }, query: { workflowModeId: workflowRepository.ownVersionMapping[props.versionId] ?.modes?.[0], viewMode: 'expert', }, }" >View </router-link> </div> </template> </bootstrap-toast> <bootstrap-toast toast-id="delete-parameter-extension-success-toast" color-class="success" > Successfully deleted Parameter Extension </bootstrap-toast> <delete-modal v-if=" workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension " modal-id="delete-parameter-extension-modal" :object-name-delete="`parameter extension of ${nameRepository.getName(props.workflowId)}@${nameRepository.getName(props.versionId)}`" @confirm-delete="deleteParameterExtension" /> <div class="d-flex justify-content-between border-bottom mb-4 pb-2"> <h2 class="w-fit"> Add parameter metadata to {{ nameRepository.getName(props.workflowId) }}@{{ nameRepository.getName(props.versionId) }} </h2> <div v-if=" workflowRepository.ownVersionMapping[props.versionId] ?.parameter_extension " > <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-parameter-extension-modal" :disabled="parameterState.loading || parameterState.makingRequest" > Delete </button> </div> </div> <div v-if="parameterState.loading" class="d-flex justify-content-center"> <div class="spinner-border" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> <form v-else ref="parameterExtensionForm" id="parameter-extension-form" class="mb-2" :class="{ 'was-validated': parameterState.formValidated }" novalidate @submit.prevent="submitForm()" > <h3>CloWM instance specific default parameters</h3> <div class="d-flex flex-wrap overflow-y-auto p-1 border border-bottom-0 rounded-top border-dashed" style="max-height: 30vh" > <b class="ms-1 w-100">Workflow parameters:</b> <template v-if="parameterPools.defaults.length > 0"> <div class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container" v-for="(param, index) in parameterPools.defaults" :key="param" @click="addDefaultParameter(param, index)" > {{ param }} </div> </template> <div v-else class="px-2 text-secondary m-1"> <i>Empty</i> </div> </div> <table class="table table-bordered align-middle"> <thead> <tr> <th scope="col"><b>Parameter</b></th> <th scope="col"><b>Value</b></th> </tr> </thead> <tbody v-if="parameterState.extension.defaults" id="defaultParamsTable"> <tr v-for="param in Object.keys(parameterState.extension.defaults)" :key="param" > <td style="width: 10%">{{ param }}</td> <td class="d-flex justify-content-between align-items-center"> <div class="flex-fill input-group"> <parameter-input :parameter="parameterSchema[param]" v-model="parameterState.extension.defaults[param]" size-modifier="sm" :resource-parameter=" parameterState.resourceParametersDefault.has(param) " force-raw-file /> </div> <button v-if=" !parameterState.resourceParametersDefault.has(param) && getParameterType(param) === 'string' " class="btn btn-primary btn-sm ms-2" type="button" @click="makeResourceParameterDefault(param)" > Resource </button> <button type="button" class="btn btn-outline-danger btn-sm ms-2" @click="deleteDefaultParameter(param)" > Remove </button> </td> </tr> </tbody> </table> <h3>Parameter Translation</h3> <div class="d-flex flex-wrap overflow-y-auto p-1 border rounded-top border-dashed" style="max-height: 30vh" > <b class="ms-1 w-100">Eligible workflow parameters:</b> <template v-if="parameterPools.mapping.length > 0"> <div class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container" v-for="(param, index) in parameterPools.mapping" :key="param" @click="addMappingParameter(param, index)" > {{ param }} </div> </template> <div v-else class="px-2 text-secondary m-1"> <i>Empty</i> </div> </div> <div v-if="parameterState.extension.mapping" class="p-0"> <div v-for="param in Object.keys(parameterState.extension.mapping)" :key="param" class="p-2 border border-top-0" > <div class="d-flex justify-content-between mb-2"> <code class="fs-6 fw-bold bg-secondary-subtle rounded p-1" >--{{ param }}</code > <button type="button" class="btn btn-outline-danger btn-sm" @click="deleteMappingParameter(param)" > Remove </button> </div> <div class="d-flex mb-5"> <div class="me-2"> <button type="button" class="btn btn-primary" :disabled=" parameterState.mappingParameterValues[param]?.length === 0 " @click=" addMappingParameterValue( param, parameterState.mappingParameterValues[param], ) " > Add Option </button> </div> <input type="text" class="form-control flex-fill w-fit" v-model="parameterState.mappingParameterValues[param]" /> </div> <template v-if="parameterState.extension.mapping[param]"> <div v-for="key in Object.keys(parameterState.extension.mapping[param])" :key="key" class="mb-5 position-relative" > <code class="p-1 position-absolute top-0 start-0 pt-0 rounded-top border-bottom-0 border border-secondary-subtle bla" >{{ key }}</code > <div class="position-absolute top-0 end-0 bla"> <span v-if="!parameterState.resourceParametersMapping.has(param)" class="p-1 me-2 rounded-top border-bottom-0 border pseudo-primary-btn border-primary-subtle cursor-pointer" @click="makeResourceParameterMapping(param, key)" >Resource</span > <span class="p-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer" @click="deleteMappingParameterValue(param, key)" >Remove</span > </div> <div class="input-group"> <parameter-input :parameter="parameterSchema[param]" force-raw-file required size-modifier="sm" border="secondary-subtle" v-model="parameterState.extension.mapping[param][key]" :resource-parameter=" parameterState.resourceParametersMapping.has(param) " /> </div> </div> </template> </div> </div> </form> <div class="d-grid gap-2"> <button type="submit" class="btn btn-success btn-lh mt-3" form="parameter-extension-form" :disabled=" parameterState.loading || parameterState.makingRequest || Object.keys(parameterState.extension).length === 0 " > Save </button> </div> </template> <style scoped> .parameter-container:hover { background: var(--bs-secondary-bg-subtle); } .pseudo-danger-btn { color: var(--bs-danger); background-color: var(--bs-white); } .pseudo-danger-btn:hover { color: var(--bs-white); background-color: var(--bs-danger); border-color: var(--bs-danger); } .pseudo-primary-btn { color: var(--bs-primary); background-color: var(--bs-white); } .pseudo-primary-btn:hover { color: var(--bs-white); background-color: var(--bs-primary); border-color: var(--bs-primary); } .bla { transform: translateY(-90%) !important; } </style>