import { computed, del, ref, set } from "vue"; import type { FieldDict, SampleSheetColumnDefinitions } from "@/api"; import { isWorkflowInput } from "@/components/Workflow/constants"; import type { CollectionTypeDescriptor } from "@/components/Workflow/Editor/modules/collectionTypeDescription"; import { getConnectionId, useConnectionStore } from "@/stores/workflowConnectionStore"; import { assertDefined } from "@/utils/assertions"; import { defineScopedStore } from "./scopedStore"; import { useWorkflowStateStore } from "./workflowEditorStateStore"; import type { Connection } from "./workflowStoreTypes"; interface StepPosition { top: number; left: number; } /* "ChangeDatatypeActionout_file1": { "action_type": "ChangeDatatypeAction", "output_name": "out_file1", "action_arguments": { "newtype": "ab1" } } */ export interface PostJobAction { action_type: string; output_name: string; action_arguments: { [index: string]: string; }; } export interface PostJobActions { [index: string]: PostJobAction; } export interface DataOutput { valid?: boolean; extensions: string[]; name: string; optional: boolean; type?: "data"; } export interface CollectionOutput extends Omit { collection: boolean; collection_type: string; collection_type_source: string | null; } export declare const ParameterTypes: "text" | "integer" | "float" | "boolean" | "color" | "data"; export interface ParameterOutput extends Omit { type: typeof ParameterTypes; parameter: true; multiple: boolean; } interface BaseStepInput { valid?: boolean; name: string; label: string; multiple: boolean; extensions: string[]; optional: boolean; input_type: string; input_subworkflow_step_id?: number; } export interface DataStepInput extends BaseStepInput { input_type: "dataset"; } export interface DataCollectionStepInput extends BaseStepInput { input_type: "dataset_collection"; collection_types: string[]; fields: FieldDict[]; column_definitions: SampleSheetColumnDefinitions; } export interface ParameterStepInput extends Omit { input_type: "parameter"; type: typeof ParameterTypes; } export type InputTerminalSource = DataStepInput | DataCollectionStepInput | ParameterStepInput; export type OutputTerminalSource = DataOutput | CollectionOutput | ParameterOutput; export type TerminalSource = InputTerminalSource | OutputTerminalSource; interface WorkflowOutput { output_name: string; label?: string | null; uuid?: string | null; } export interface NewStep { annotation?: string; config_form?: { [index: string]: any }; content_id?: string | null; id?: number; errors?: string[] | null; input_connections: StepInputConnection; inputs: Array; label?: string | null; name: string; outputs: Array; position?: StepPosition; post_job_actions?: PostJobActions; tool_id?: string; tool_uuid?: string; tool_state: Record; tool_version?: string; tooltip?: string | null; type: "tool" | "data_input" | "data_collection_input" | "subworkflow" | "parameter_input" | "pause"; uuid?: string; when?: string | null; workflow_id?: string; workflow_outputs?: WorkflowOutput[]; } export interface Step extends NewStep { id: number; } export interface Steps { [index: string]: Step; } export interface StepInputConnection { [index: string]: ConnectionOutputLink | ConnectionOutputLink[] | undefined; } export interface ConnectionOutputLink { output_name: string; id: number; input_subworkflow_step_id?: number; } export interface WorkflowOutputs { [index: string]: { stepId: number; outputName: string; }; } interface StepInputMapOver { [index: number]: { [index: string]: CollectionTypeDescriptor }; } export type WorkflowStepStore = ReturnType; /** * Returns combined step inputs: extra inputs (e.g., "when" conditionals) followed by regular inputs. * This is the single source of truth for all step inputs. */ export function getCombinedStepInputs(step: Step, stepStore: WorkflowStepStore): InputTerminalSource[] { const extraInputs = stepStore.getStepExtraInputs(step.id); return [...extraInputs, ...step.inputs]; } export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (workflowId) => { const steps = ref({}); const stepMapOver = ref<{ [index: number]: CollectionTypeDescriptor }>({}); const stepInputMapOver = ref({}); const stepIndex = ref(-1); const stepExtraInputs = ref<{ [index: number]: InputTerminalSource[] }>({}); function $reset() { steps.value = {}; stepMapOver.value = {}; stepInputMapOver.value = {}; stepIndex.value = -1; stepExtraInputs.value = {}; } const getStep = computed(() => (stepId: number) => steps.value[stepId.toString()]); const getStepExtraInputs = computed(() => (stepId: number) => stepExtraInputs.value[stepId] || []); const getStepIndex = computed(() => Math.max(...Object.values(steps.value).map((step) => step.id), stepIndex.value), ); const hasActiveOutputs = computed(() => Boolean(Object.values(steps.value).find((step) => step.workflow_outputs?.length)), ); const hasInputSteps = computed(() => Boolean(Object.values(steps.value).find((step) => isWorkflowInput(step.type))), ); const workflowOutputs = computed(() => { const workflowOutputs: WorkflowOutputs = {}; Object.values(steps.value).forEach((step) => { if (step.workflow_outputs?.length) { step.workflow_outputs.forEach((workflowOutput) => { if (workflowOutput.label) { workflowOutputs[workflowOutput.label] = { outputName: workflowOutput.output_name, stepId: step.id, }; } }); } }); return workflowOutputs; }); const duplicateLabels = computed(() => { const duplicateLabels: Set = new Set(); const labels: Set = new Set(); Object.values(steps.value).forEach((step) => { if (step.workflow_outputs?.length) { step.workflow_outputs.forEach((workflowOutput) => { if (workflowOutput.label) { if (labels.has(workflowOutput.label)) { duplicateLabels.add(workflowOutput.label); } labels.add(workflowOutput.label); } }); } }); return duplicateLabels; }); const connectionStore = useConnectionStore(workflowId); const stateStore = useWorkflowStateStore(workflowId); function addStep(newStep: NewStep, select = false, createConnections = true): Step { const stepId = newStep.id ?? getStepIndex.value + 1; const step = Object.freeze({ ...newStep, id: stepId } as Step); set(steps.value, stepId.toString(), step); if (createConnections) { stepToConnections(step).forEach((connection) => connectionStore.addConnection(connection)); } stepExtraInputs.value[step.id] = findStepExtraInputs(step); if (select) { stateStore.setStepMultiSelected(step.id, true); } return step; } function insertNewStep( contentId: NewStep["content_id"], name: NewStep["name"], type: NewStep["type"], position: NewStep["position"], ) { const stepData: NewStep = { name: name, content_id: contentId, input_connections: {}, type: type, inputs: [], outputs: [], position: position, post_job_actions: {}, tool_state: {}, }; return addStep(stepData); } function updateStep(step: Step) { const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) => step.outputs.find((output) => workflowOutput.output_name == output.name), ); steps.value[step.id.toString()] = Object.freeze({ ...step, workflow_outputs }); stepExtraInputs.value[step.id] = findStepExtraInputs(step); } function updateStepValue(stepId: number, key: K, value: Step[K]) { const step = steps.value[stepId]; assertDefined(step); const partialStep: Partial = {}; partialStep[key] = value; updateStep({ ...step, ...partialStep }); } function changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) { set(stepMapOver.value, stepId, mapOver); } function resetStepInputMapOver(stepId: number) { set(stepInputMapOver.value, stepId, {}); } function changeStepInputMapOver(stepId: number, inputName: string, mapOver: CollectionTypeDescriptor) { if (stepInputMapOver.value[stepId]) { set(stepInputMapOver.value[stepId]!, inputName, mapOver); } else { set(stepInputMapOver.value, stepId, { [inputName]: mapOver }); } } function addConnection(connection: Connection) { const inputStep = getStep.value(connection.input.stepId); assertDefined( inputStep, `Failed to add connection, because step with id ${connection.input.stepId} is undefined`, ); const input = inputStep.inputs.find((input) => input.name === connection.input.name); const connectionLink: ConnectionOutputLink = { output_name: connection.output.name, id: connection.output.stepId, }; if (input && "input_subworkflow_step_id" in input && input.input_subworkflow_step_id !== undefined) { connectionLink["input_subworkflow_step_id"] = input.input_subworkflow_step_id; } let connectionLinks: ConnectionOutputLink[] = [connectionLink]; let inputConnection = inputStep.input_connections[connection.input.name]; if (inputConnection) { if (!Array.isArray(inputConnection)) { inputConnection = [inputConnection]; } inputConnection = inputConnection.filter( (connection) => !(connection.id === connectionLink.id && connection.output_name === connectionLink.output_name), ); connectionLinks = [...connectionLinks, ...inputConnection]; } const updatedStep = { ...inputStep, input_connections: { ...inputStep.input_connections, [connection.input.name]: connectionLinks.sort((a, b) => a.id === b.id ? a.output_name.localeCompare(b.output_name) : a.id - b.id, ), }, }; updateStep(updatedStep); } function removeConnection(connection: Connection) { const inputStep = getStep.value(connection.input.stepId); assertDefined( inputStep, `Failed to remove connection, because step with id ${connection.input.stepId} is undefined`, ); const inputConnections = inputStep.input_connections[connection.input.name]; if (getStepExtraInputs.value(inputStep.id).find((input) => connection.input.name === input.name)) { inputStep.input_connections[connection.input.name] = undefined; } else { if (Array.isArray(inputConnections)) { inputStep.input_connections[connection.input.name] = inputConnections.filter( (outputLink) => !(outputLink.id === connection.output.stepId, outputLink.output_name === connection.output.name), ); } else { del(inputStep.input_connections, connection.input.name); } } updateStep(inputStep); } const { deleteStepPosition, deleteStepTerminals } = useWorkflowStateStore(workflowId); function removeStep(stepId: number) { connectionStore .getConnectionsForStep(stepId) .forEach((connection) => connectionStore.removeConnection(getConnectionId(connection))); del(steps.value, stepId.toString()); del(stepExtraInputs.value, stepId); del(stateStore.multiSelectedSteps, stepId); del(stepMapOver.value, stepId.toString()); deleteStepPosition(stepId); deleteStepTerminals(stepId); } return { steps, stepMapOver, stepInputMapOver, stepIndex, stepExtraInputs, $reset, getStep, getStepExtraInputs, getStepIndex, hasActiveOutputs, hasInputSteps, workflowOutputs, duplicateLabels, addStep, insertNewStep, updateStep, updateStepValue, changeStepMapOver, resetStepInputMapOver, changeStepInputMapOver, addConnection, removeConnection, removeStep, }; }); function makeConnection(inputId: number, inputName: string, outputId: number, outputName: string): Connection { return { input: { stepId: inputId, name: inputName, connectorType: "input", }, output: { stepId: outputId, name: outputName, connectorType: "output", }, }; } function stepToConnections(step: Step): Connection[] { const connections: Connection[] = []; if (step.input_connections) { Object.entries(step?.input_connections).forEach(([inputName, outputArray]) => { if (outputArray === undefined) { return; } if (!Array.isArray(outputArray)) { outputArray = [outputArray]; } outputArray.forEach((output) => { const connection = makeConnection(step.id, inputName, output.id, output.output_name); const connectionInput = step.inputs.find((input) => input.name == inputName); if (connectionInput && "input_subworkflow_step_id" in connectionInput) { connection.input.input_subworkflow_step_id = connectionInput.input_subworkflow_step_id; } connections.push(connection); }); }); } return connections; } function findStepExtraInputs(step: Step) { const extraInputs: InputTerminalSource[] = []; if (step.when !== undefined) { Object.keys(step.input_connections).forEach((inputName) => { if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) { const terminalSource = { name: inputName, optional: false, input_type: "parameter" as const, type: "boolean" as const, multiple: false, label: inputName, extensions: [], }; extraInputs.push(terminalSource); } }); } return extraInputs; }