diff --git a/package-lock.json b/package-lock.json index 87d4ff0688099395e11f5523d084e53c1955ddfd..2dfeaeda58ab28cdd0d100a57ff5a86d09ae4ba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6553,9 +6553,9 @@ } }, "node_modules/vite": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz", - "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", "dev": true, "dependencies": { "esbuild": "^0.19.3", diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue index 1afe153268f3556139471f3abe4cf5d277dcd90a..1e8e19e97ca32699d60c8bafb8c678db477f525b 100644 --- a/src/components/workflows/WorkflowWithVersionsCard.vue +++ b/src/components/workflows/WorkflowWithVersionsCard.vue @@ -325,6 +325,24 @@ onMounted(() => { >Update icon</a > </li> + <li> + <router-link + class="dropdown-item" + :to="{ + name: 'workflow-parameter-translation', + params: { + workflowId: props.workflow.workflow_id, + versionId: version.workflow_version_id, + }, + }" + > + <template v-if="version.parameter_extension" + >Update</template + > + <template v-else>Add</template> + Parameter Translation + </router-link> + </li> </ul> </td> </tr> diff --git a/src/router/workflowRoutes.ts b/src/router/workflowRoutes.ts index 5eb83dbb0a380052ba031e92b649b32165b5f32a..c7d238ecef33d463569d43d7ed56d8307b1b8187 100644 --- a/src/router/workflowRoutes.ts +++ b/src/router/workflowRoutes.ts @@ -97,4 +97,14 @@ export const workflowRoutes: RouteRecordRaw[] = [ }, ], }, + { + path: "workflows/:workflowId/version/:versionId/parameters", + name: "workflow-parameter-translation", + component: () => + import("../views/workflows/CreateParameterTranslationView.vue"), + props: (route) => ({ + versionId: route.params.versionId, + workflowId: route.params.workflowId, + }), + }, ]; diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts index b094280840e4f9f43ae78d36e712c47d2e4b8e6e..8ecfdb5f732b011a080264335324331d5e6246d4 100644 --- a/src/stores/workflows.ts +++ b/src/stores/workflows.ts @@ -63,6 +63,15 @@ export const useWorkflowStore = defineStore({ } return mapping; }, + ownVersionMapping(): Record<string, WorkflowVersion> { + const mapping: Record<string, WorkflowVersion> = {}; + for (const workflow of this.ownWorkflows) { + for (const version of workflow.versions) { + mapping[version.workflow_version_id] = version; + } + } + return mapping; + }, getArbitraryWorkflow(): (wid: string) => Promise<WorkflowIn | undefined> { return (wid: string) => get(wid); }, @@ -71,6 +80,15 @@ export const useWorkflowStore = defineStore({ __addNameToMapping(key: string, value: string) { useNameStore().addNameToMapping(key, value); }, + fetchWorkflowVersion(wid: string, vid: string): Promise<WorkflowVersion> { + return WorkflowVersionService.workflowVersionGetWorkflowVersion( + vid, + wid, + ).then((version) => { + this.__addNameToMapping(version.workflow_version_id, version.version); + return version; + }); + }, fetchWorkflows(onFinally?: () => void): Promise<WorkflowOut[]> { if (Object.keys(this.workflowMapping).length > 0) { onFinally?.(); diff --git a/src/views/workflows/CreateParameterTranslationView.vue b/src/views/workflows/CreateParameterTranslationView.vue new file mode 100644 index 0000000000000000000000000000000000000000..5cc296f746e446b481b32a31bd48e5542346715c --- /dev/null +++ b/src/views/workflows/CreateParameterTranslationView.vue @@ -0,0 +1,287 @@ +<script setup lang="ts"> +import { useNameStore } from "@/stores/names"; +import { useWorkflowStore } from "@/stores/workflows"; +import { computed, onMounted, reactive, watch } from "vue"; +import { + DocumentationEnum, + type ParameterExtension_Input, +} from "@/client/workflow"; + +const props = defineProps<{ + versionId: string; + workflowId: string; +}>(); + +const parameterState = reactive<{ + extension: ParameterExtension_Input; + resourceParameters: Set<string>; +}>({ + extension: {}, + resourceParameters: new Set(), +}); + +const parameterPools = reactive<{ + defaults: string[]; + mapping: string[]; +}>({ + defaults: [], + mapping: [], +}); + +const nameRepository = useNameStore(); +const workflowRepository = useWorkflowStore(); + +watch( + () => workflowRepository.documentationFiles[props.versionId ?? ""], + (newVal, old) => { + if (newVal != old && newVal?.parameter_schema != undefined) { + updateParameterPools(newVal); + } + }, + { + deep: true, + }, +); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function updateParameterPools(newVal?: Record<DocumentationEnum, any>) { + if (newVal?.parameter_schema) { + const parameters = extractParameterList(newVal.parameter_schema); + parameterPools.defaults = parameters.slice(); + parameterPools.mapping = parameters.slice(); + } + 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, + ); + } +} + +// 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 addDefaultParameter(param: string, index: number) { + if (parameterState.extension.defaults == undefined) { + parameterState.extension.defaults = {}; + } + parameterPools.defaults.splice(index, 1); + switch (getParameterType(param)) { + case "integer": { + parameterState.extension.defaults[param] = 0; + break; + } + case "number": { + parameterState.extension.defaults[param] = 0; + break; + } + case "boolean": { + parameterState.extension.defaults[param] = true; + break; + } + case "string": { + parameterState.extension.defaults[param] = + parameterSchema.value[param]?.["enum"]?.[0] ?? ""; + break; + } + default: { + parameterState.extension.defaults[param] = ""; + break; + } + } +} + +function deleteDefaultParameter(param: string) { + delete parameterState.extension.defaults?.[param]; + parameterPools.defaults.push(param); + if (Object.keys(parameterState.extension.defaults ?? {}).length === 0) { + parameterState.extension.defaults = undefined; + } +} + +// 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; +}); + +onMounted(() => { + workflowRepository.fetchWorkflow(props.workflowId, true, () => { + parameterState.extension = + workflowRepository.ownVersionMapping[props.versionId] + ?.parameter_extension ?? {}; + workflowRepository + .fetchWorkflowDocumentation( + props.workflowId, + props.versionId, + DocumentationEnum.PARAMETER_SCHEMA, + workflowRepository.ownVersionMapping[props.versionId]?.modes?.[0], + ) + .finally(() => { + updateParameterPools( + workflowRepository.documentationFiles[props.versionId], + ); + }); + }); +}); +</script> + +<template> + <div class="row border-bottom mb-4"> + <h2 class="mb-2"> + Add parameter metadata to + {{ nameRepository.getName(props.workflowId) }}@{{ + nameRepository.getName(props.versionId) + }} + </h2> + </div> + <div + class="d-flex flex-wrap overflow-y-auto p-1 border rounded border-dashed mb-2" + style="max-height: 30vh" + v-if="parameterPools.defaults.length > 0" + > + <b class="ms-1 w-100">Workflow parameters:</b> + <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> + </div> + <table class="table table-bordered"> + <thead> + <tr> + <th scope="col"><b>Parameter</b></th> + <th scope="col"><b>Value</b></th> + </tr> + </thead> + <tbody v-if="parameterState.extension.defaults"> + <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"> + <input + v-if=" + getParameterType(param) === 'number' || + getParameterType(param) === 'integer' + " + type="number" + class="form-control form-control-sm flex-grow" + v-model="parameterState.extension.defaults[param]" + :step="getParameterType(param) === 'integer' ? 1 : 0.0001" + :min="parameterSchema[param]['minimum']" + :max="parameterSchema[param]['maximum']" + /> + <div + v-else-if="getParameterType(param) === 'boolean'" + class="flex-grow" + > + <div class="form-check form-check-inline"> + <label + class="form-check-label" + :for="'trueOption' + param.replace(/\./g, '')" + >True</label + > + <input + class="form-check-input" + type="radio" + :name="'inlineRadioOptions' + param.replace(/\./g, '')" + :id="'trueOption' + param.replace(/\./g, '')" + :value="true" + v-model="parameterState.extension.defaults[param]" + /> + </div> + <div class="form-check form-check-inline"> + <input + class="form-check-input" + type="radio" + :name="'inlineRadioOptions' + param.replace(/\./g, '')" + :id="'falseOption' + param.replace(/\./g, '')" + :value="false" + v-model="parameterState.extension.defaults[param]" + /> + <label + class="form-check-label" + :for="'falseOption' + param.replace(/\./g, '')" + >False</label + > + </div> + </div> + <select + v-else-if="parameterSchema[param]?.['enum']" + class="form-select form-select-sm flex-grow" + v-model="parameterState.extension.defaults[param]" + > + <option + v-for="option in parameterSchema[param]?.['enum']" + :key="option" + :value="option" + > + {{ option }} + </option> + </select> + <div v-else> + <input + type="text" + class="form-control form-control-sm flex-grow" + v-model="parameterState.extension.defaults[param]" + :pattern="parameterSchema[param]?.['pattern']" + /> + </div> + <button + type="button" + class="btn btn-outline-danger btn-sm ms-2" + @click="deleteDefaultParameter(param)" + > + Remove + </button> + </td> + </tr> + </tbody> + </table> + <pre><code>{{ + parameterState.extension + }}</code></pre> + <pre><code>{{ parameterPools }}</code></pre> +</template> + +<style scoped> +.parameter-container:hover { + background: var(--bs-secondary-bg-subtle); +} +</style>