diff --git a/client/src/components/Workflow/Editor/ConnectionMenu.vue b/client/src/components/Workflow/Editor/ConnectionMenu.vue index 63c7e1bed780..ec2c875d23b6 100644 --- a/client/src/components/Workflow/Editor/ConnectionMenu.vue +++ b/client/src/components/Workflow/Editor/ConnectionMenu.vue @@ -25,7 +25,7 @@ import { computed, type ComputedRef, onMounted, ref, watch } from "vue"; import { useFocusWithin } from "@/composables/useActiveElement"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; import { assertDefined } from "@/utils/assertions"; import { @@ -62,7 +62,7 @@ watch(focused, (focused) => { } }); -const stepStore = useWorkflowStepStore(); +const { connectionStore, stepStore } = useWorkflowStores(); interface InputObject { stepId: number; @@ -97,7 +97,7 @@ function inputObjectToTerminal(inputObject: InputObject): InputTerminals { const step = stepStore.getStep(inputObject.stepId); assertDefined(step); const inputSource = step.inputs.find((input) => input.name == inputObject.inputName)!; - return terminalFactory(inputObject.stepId, inputSource, props.terminal.datatypesMapper); + return terminalFactory(inputObject.stepId, inputSource, props.terminal.datatypesMapper, connectionStore, stepStore); } const validInputs: ComputedRef = computed(() => { diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.test.js b/client/src/components/Workflow/Editor/Forms/FormDefault.test.js index cc77e0ca5dc2..2a09dc0d7963 100644 --- a/client/src/components/Workflow/Editor/Forms/FormDefault.test.js +++ b/client/src/components/Workflow/Editor/Forms/FormDefault.test.js @@ -34,6 +34,9 @@ describe("FormDefault", () => { }, localVue, pinia: createTestingPinia(), + provide: { + workflowId: "mock-workflow", + }, }); }); diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.vue b/client/src/components/Workflow/Editor/Forms/FormDefault.vue index fa98d5a19aec..8ec5d71f984f 100644 --- a/client/src/components/Workflow/Editor/Forms/FormDefault.vue +++ b/client/src/components/Workflow/Editor/Forms/FormDefault.vue @@ -62,7 +62,8 @@ import { computed, toRef } from "vue"; import type { DatatypesMapperModel } from "@/components/Datatypes/model"; import WorkflowIcons from "@/components/Workflow/icons"; -import { type Step, useWorkflowStepStore } from "@/stores/workflowStepStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; +import type { Step } from "@/stores/workflowStepStore"; import { useStepProps } from "../composables/useStepProps"; import { useUniqueLabelError } from "../composables/useUniqueLabelError"; @@ -80,7 +81,7 @@ const props = defineProps<{ const emit = defineEmits(["onAnnotation", "onLabel", "onAttemptRefactor", "onEditSubworkflow", "onSetData"]); const stepRef = toRef(props, "step"); const { stepId, contentId, annotation, label, name, type, configForm } = useStepProps(stepRef); -const stepStore = useWorkflowStepStore(); +const { stepStore } = useWorkflowStores(); const uniqueErrorLabel = useUniqueLabelError(stepStore, label.value); const stepTitle = computed(() => { if (label.value) { diff --git a/client/src/components/Workflow/Editor/Forms/FormOutputLabel.test.js b/client/src/components/Workflow/Editor/Forms/FormOutputLabel.test.js index fac883ec8a72..6bc51c9be00b 100644 --- a/client/src/components/Workflow/Editor/Forms/FormOutputLabel.test.js +++ b/client/src/components/Workflow/Editor/Forms/FormOutputLabel.test.js @@ -29,7 +29,9 @@ describe("FormOutputLabel", () => { }, localVue, pinia, + provide: { workflowId: "mock-workflow" }, }); + const stepTwo = { id: 1, outputs: [{ name: "other-name" }], workflow_outputs: outputs }; wrapperOther = mount(FormOutputLabel, { propsData: { @@ -38,8 +40,9 @@ describe("FormOutputLabel", () => { }, localVue, pinia, + provide: { workflowId: "mock-workflow" }, }); - stepStore = useWorkflowStepStore(); + stepStore = useWorkflowStepStore("mock-workflow"); stepStore.addStep(stepOne); stepStore.addStep(stepTwo); }); diff --git a/client/src/components/Workflow/Editor/Forms/FormOutputLabel.vue b/client/src/components/Workflow/Editor/Forms/FormOutputLabel.vue index 53fb2ce2d44d..7225b787b0d2 100644 --- a/client/src/components/Workflow/Editor/Forms/FormOutputLabel.vue +++ b/client/src/components/Workflow/Editor/Forms/FormOutputLabel.vue @@ -13,8 +13,8 @@ import type { Ref } from "vue"; import { computed, ref } from "vue"; +import { useWorkflowStores } from "@/composables/workflowStores"; import type { Step } from "@/stores/workflowStepStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; import FormElement from "@/components/Form/FormElement.vue"; @@ -29,7 +29,7 @@ const props = withDefaults( } ); -const stepStore = useWorkflowStepStore(); +const { stepStore } = useWorkflowStores(); const error: Ref = ref(undefined); const id = computed(() => `__label__${props.name}`); diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.test.js b/client/src/components/Workflow/Editor/Forms/FormTool.test.js index ccbfa2dde738..97416e1268a6 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.test.js +++ b/client/src/components/Workflow/Editor/Forms/FormTool.test.js @@ -48,6 +48,7 @@ describe("FormTool", () => { ToolFooter: { template: "
tool-footer
" }, }, pinia: createTestingPinia(), + provide: { workflowId: "mock-workflow" }, }); } diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.vue b/client/src/components/Workflow/Editor/Forms/FormTool.vue index c4760b6785ba..0a10578d2e8d 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.vue +++ b/client/src/components/Workflow/Editor/Forms/FormTool.vue @@ -56,7 +56,7 @@ import Utils from "utils/utils"; import { toRef } from "vue"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; import { useStepProps } from "../composables/useStepProps"; import { useUniqueLabelError } from "../composables/useUniqueLabelError"; @@ -93,7 +93,7 @@ export default { const { stepId, annotation, label, stepInputs, stepOutputs, configForm, postJobActions } = useStepProps( toRef(props, "step") ); - const stepStore = useWorkflowStepStore(); + const { stepStore } = useWorkflowStores(); const uniqueErrorLabel = useUniqueLabelError(stepStore, label); return { diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 9fd7781babc7..bddaa6391418 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -166,11 +166,9 @@ import Vue, { computed, onUnmounted, ref } from "vue"; import { getUntypedWorkflowParameters } from "@/components/Workflow/Editor/modules/parameters"; import { ConfirmDialog } from "@/composables/confirmDialog"; import { useDatatypesMapper } from "@/composables/datatypesMapper"; +import { provideScopedWorkflowStores } from "@/composables/workflowStores"; import { hide_modal } from "@/layout/modal"; import { getAppRoot } from "@/onload/loadConfig"; -import { useConnectionStore } from "@/stores/workflowConnectionStore"; -import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; import { LastQueue } from "@/utils/promise-queue"; import { defaultPosition } from "./composables/useDefaultStepPosition"; @@ -235,10 +233,10 @@ export default { }, setup(props, { emit }) { const { datatypes, datatypesMapper, datatypesMapperLoading } = useDatatypesMapper(); - const connectionsStore = useConnectionStore(); - const stepStore = useWorkflowStepStore(); + + const { connectionStore, stepStore, stateStore } = provideScopedWorkflowStores(props.id); + const { getStepIndex, steps } = storeToRefs(stepStore); - const stateStore = useWorkflowStateStore(); const { activeNodeId } = storeToRefs(stateStore); const activeStep = computed(() => { if (activeNodeId.value !== null) { @@ -248,14 +246,14 @@ export default { }); const hasChanges = ref(false); - const hasInvalidConnections = computed(() => Object.keys(connectionsStore.invalidConnections).length > 0); + const hasInvalidConnections = computed(() => Object.keys(connectionStore.invalidConnections).length > 0); stepStore.$subscribe((mutation, state) => { hasChanges.value = true; }); function resetStores() { - connectionsStore.$reset(); + connectionStore.$reset(); stepStore.$reset(); stateStore.$reset(); } @@ -264,7 +262,7 @@ export default { emit("update:confirmation", false); }); return { - connectionsStore, + connectionStore, hasChanges, hasInvalidConnections, stepStore, @@ -360,7 +358,7 @@ export default { this.onUpdateStep(step); }, onConnect(connection) { - this.connectionsStore.addConnection(connection); + this.connectionStore.addConnection(connection); }, onAttemptRefactor(actions) { if (this.hasChanges) { @@ -406,7 +404,7 @@ export default { }, async onRefactor(response) { this.resetStores(); - await fromSimple(response.workflow); + await fromSimple(this.id, response.workflow); this._loadEditorData(response.workflow); }, onUpdate(step) { @@ -471,7 +469,7 @@ export default { // Load workflow definition this.onWorkflowMessage("Importing workflow", "progress"); loadWorkflow({ id }).then((data) => { - fromSimple(data, true, defaultPosition(this.graphOffset, this.transform)); + fromSimple(this.id, data, true, defaultPosition(this.graphOffset, this.transform)); // Determine if any parameters were 'upgraded' and provide message const insertedStateMessages = getStateUpgradeMessages(data); this.onInsertedStateMessages(insertedStateMessages); @@ -523,7 +521,7 @@ export default { }, onLayout() { return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => { - layout.autoLayout(this.steps).then((newSteps) => { + layout.autoLayout(this.id, this.steps).then((newSteps) => { newSteps.map((step) => this.onUpdateStep(step)); }); }); @@ -691,7 +689,7 @@ export default { this.lastQueue .enqueue(loadWorkflow, { id, version }) .then((data) => { - fromSimple(data); + fromSimple(id, data); this._loadEditorData(data); }) .catch((response) => { diff --git a/client/src/components/Workflow/Editor/Lint.test.js b/client/src/components/Workflow/Editor/Lint.test.js index 948d63252cbe..6c8e516d428f 100644 --- a/client/src/components/Workflow/Editor/Lint.test.js +++ b/client/src/components/Workflow/Editor/Lint.test.js @@ -1,6 +1,6 @@ import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; -import { PiniaVuePlugin } from "pinia"; +import { PiniaVuePlugin, setActivePinia } from "pinia"; import { getLocalVue } from "tests/jest/helpers"; import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; @@ -88,6 +88,9 @@ describe("Lint", () => { let stepStore; beforeEach(() => { + const pinia = createTestingPinia({ stubActions: false }); + setActivePinia(pinia); + wrapper = mount(Lint, { propsData: { untypedParameters: getUntypedWorkflowParameters(steps), @@ -98,9 +101,11 @@ describe("Lint", () => { datatypesMapper: testDatatypesMapper, }, localVue, - pinia: createTestingPinia({ stubActions: false }), + pinia, + provide: { workflowId: "mock-workflow" }, }); - stepStore = useWorkflowStepStore(); + + stepStore = useWorkflowStepStore("mock-workflow"); Object.values(steps).map((step) => stepStore.addStep(step)); }); @@ -131,7 +136,7 @@ describe("Lint", () => { }); it("should fire refactor event to extract untyped parameter and remove unlabeled workflows", async () => { - wrapper.vm.onRefactor(); + await wrapper.find(".refactor-button").trigger("click"); expect(wrapper.emitted().onRefactor.length).toBe(1); const actions = wrapper.emitted().onRefactor[0][0]; expect(actions.length).toBe(2); @@ -139,9 +144,10 @@ describe("Lint", () => { expect(actions[0].name).toBe("untyped_parameter"); expect(actions[1].action_type).toBe("remove_unlabeled_workflow_outputs"); }); + it("should include connect input action when input disconnected", async () => { stepStore.removeStep(0); - wrapper.vm.onRefactor(); + await wrapper.find(".refactor-button").trigger("click"); expect(wrapper.emitted().onRefactor.length).toBe(1); const actions = wrapper.emitted().onRefactor[0][0]; expect(actions.length).toBe(3); diff --git a/client/src/components/Workflow/Editor/Lint.vue b/client/src/components/Workflow/Editor/Lint.vue index 0463c56869ee..5ab31d273263 100644 --- a/client/src/components/Workflow/Editor/Lint.vue +++ b/client/src/components/Workflow/Editor/Lint.vue @@ -6,7 +6,7 @@ Best Practices Review
- Try to automatically fix issues. + Try to automatically fix issues.
@@ -85,7 +85,7 @@ import { storeToRefs } from "pinia"; import Vue from "vue"; import { DatatypesMapperModel } from "@/components/Datatypes/model"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; import { fixAllIssues, @@ -135,8 +135,9 @@ export default { }, }, setup() { - const { hasActiveOutputs } = storeToRefs(useWorkflowStepStore()); - return { hasActiveOutputs }; + const { connectionStore, stepStore } = useWorkflowStores(); + const { hasActiveOutputs } = storeToRefs(stepStore); + return { connectionStore, stepStore, hasActiveOutputs }; }, computed: { showRefactor() { @@ -170,7 +171,7 @@ export default { return getUntypedParameters(this.untypedParameters); }, warningDisconnectedInputs() { - return getDisconnectedInputs(this.steps, this.datatypesMapper); + return getDisconnectedInputs(this.steps, this.datatypesMapper, this.connectionStore, this.stepStore); }, warningMissingMetadata() { return getMissingMetadata(this.steps); @@ -226,7 +227,13 @@ export default { this.$emit("onUnhighlight", item.stepId); }, onRefactor() { - const actions = fixAllIssues(this.steps, this.untypedParameters); + const actions = fixAllIssues( + this.steps, + this.untypedParameters, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); this.$emit("onRefactor", actions); }, }, diff --git a/client/src/components/Workflow/Editor/Node.test.ts b/client/src/components/Workflow/Editor/Node.test.ts index 7eebf5a3047e..959a38e65869 100644 --- a/client/src/components/Workflow/Editor/Node.test.ts +++ b/client/src/components/Workflow/Editor/Node.test.ts @@ -18,7 +18,7 @@ describe("Node", () => { it("test attributes", async () => { const testingPinia = createTestingPinia(); setActivePinia(testingPinia); - const wrapper = shallowMount(Node, { + const wrapper = shallowMount(Node as any, { propsData: { id: 0, contentId: "tool name", @@ -30,6 +30,9 @@ describe("Node", () => { }, localVue, pinia: testingPinia, + provide: { + workflowId: "mock-workflow", + }, }); await flushPromises(); // fa-wrench is the tool icon ... diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue index 1c2ce3144148..8919f8276061 100644 --- a/client/src/components/Workflow/Editor/Node.vue +++ b/client/src/components/Workflow/Editor/Node.vue @@ -120,10 +120,9 @@ import { getGalaxyInstance } from "@/app"; import { DatatypesMapperModel } from "@/components/Datatypes/model"; import { useNodePosition } from "@/components/Workflow/Editor/composables/useNodePosition"; import WorkflowIcons from "@/components/Workflow/icons"; -import { useConnectionStore } from "@/stores/workflowConnectionStore"; -import { type TerminalPosition, useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; +import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore"; import type { Step } from "@/stores/workflowStepStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; import type { OutputTerminals } from "./modules/terminals"; @@ -180,9 +179,7 @@ const elHtml: Ref = computed(() => (el.value?.$el as HTMLEle const postJobActions = computed(() => props.step.post_job_actions || {}); const workflowOutputs = computed(() => props.step.workflow_outputs || []); -const connectionStore = useConnectionStore(); -const stateStore = useWorkflowStateStore(); -const stepStore = useWorkflowStepStore(); +const { connectionStore, stateStore, stepStore } = useWorkflowStores(); const isLoading = computed(() => Boolean(stateStore.getStepLoadingState(props.id)?.loading)); useNodePosition( elHtml, diff --git a/client/src/components/Workflow/Editor/NodeInput.vue b/client/src/components/Workflow/Editor/NodeInput.vue index 48fc498ee0a2..f5992017ebaa 100644 --- a/client/src/components/Workflow/Editor/NodeInput.vue +++ b/client/src/components/Workflow/Editor/NodeInput.vue @@ -33,8 +33,8 @@ import { computed, inject, ref, toRefs, watch, watchEffect } from "vue"; import { DatatypesMapperModel } from "@/components/Datatypes/model"; import { ConnectionAcceptable, terminalFactory } from "@/components/Workflow/Editor/modules/terminals"; -import { getConnectionId, useConnectionStore } from "@/stores/workflowConnectionStore"; -import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; +import { getConnectionId } from "@/stores/workflowConnectionStore"; import { useRelativePosition } from "./composables/relativePosition"; import { useTerminal } from "./composables/useTerminal"; @@ -85,7 +85,7 @@ export default { const isDragging = inject("isDragging"); const id = computed(() => `node-${props.stepId}-input-${props.input.name}`); const iconId = computed(() => `${id.value}-icon`); - const connectionStore = useConnectionStore(); + const { connectionStore, stateStore, stepStore } = useWorkflowStores(); const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, input, datatypesMapper); const hasTerminals = ref(false); @@ -101,7 +101,6 @@ export default { return classes; }); - const stateStore = useWorkflowStateStore(); const { draggingTerminal } = storeToRefs(stateStore); const terminalIsHovered = ref(false); @@ -153,6 +152,7 @@ export default { hasTerminals, terminal, stateStore, + stepStore, isMultiple, showRemove, terminalIsHovered, @@ -214,7 +214,13 @@ export default { }, onDrop(e) { const stepOut = JSON.parse(e.dataTransfer.getData("text/plain")); - const droppedTerminal = terminalFactory(stepOut.stepId, stepOut.output, this.datatypesMapper); + const droppedTerminal = terminalFactory( + stepOut.stepId, + stepOut.output, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); this.$root.$emit("bv::hide::tooltip", this.iconId); if (this.terminal.canAccept(droppedTerminal).canAccept) { this.terminal.connect(droppedTerminal); diff --git a/client/src/components/Workflow/Editor/NodeOutput.test.ts b/client/src/components/Workflow/Editor/NodeOutput.test.ts index 4d7d7863ec80..964c398c68b7 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.test.ts +++ b/client/src/components/Workflow/Editor/NodeOutput.test.ts @@ -4,6 +4,7 @@ import { getLocalVue } from "tests/jest/helpers"; import { nextTick, ref } from "vue"; import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; +import { useConnectionStore } from "@/stores/workflowConnectionStore"; import { type Step, type Steps, useWorkflowStepStore } from "@/stores/workflowStepStore"; import { terminalFactory } from "./modules/terminals"; @@ -51,36 +52,51 @@ const transform = ref({ x: 0, y: 0, k: 1 }); describe("NodeOutput", () => { let pinia: ReturnType; let stepStore: ReturnType; + let connectionStore: ReturnType; beforeEach(() => { pinia = createPinia(); setActivePinia(pinia); - stepStore = useWorkflowStepStore(); + stepStore = useWorkflowStepStore("mock-workflow"); + connectionStore = useConnectionStore("mock-workflow"); Object.values(advancedSteps).map((step) => stepStore.addStep(step)); }); it("does not display multiple icon if not mapped over", async () => { const simpleDataStep = stepForLabel("simple data", stepStore.steps); const propsData = propsForStep(simpleDataStep); - const wrapper = shallowMount(NodeOutput, { + const wrapper = shallowMount(NodeOutput as any, { propsData: propsData, localVue, pinia, - provide: { transform }, + provide: { transform, workflowId: "mock-workflow" }, }); expect(wrapper.find(".multiple").exists()).toBe(false); }); + it("displays multiple icon if not mapped over", async () => { const simpleDataStep = stepForLabel("simple data", stepStore.steps); const listInputStep = stepForLabel("list input", stepStore.steps); - const inputTerminal = terminalFactory(simpleDataStep.id, simpleDataStep.inputs[0]!, testDatatypesMapper); - const outputTerminal = terminalFactory(listInputStep.id, listInputStep.outputs[0]!, testDatatypesMapper); + const inputTerminal = terminalFactory( + simpleDataStep.id, + simpleDataStep.inputs[0]!, + testDatatypesMapper, + connectionStore, + stepStore + ); + const outputTerminal = terminalFactory( + listInputStep.id, + listInputStep.outputs[0]!, + testDatatypesMapper, + connectionStore, + stepStore + ); const propsData = propsForStep(simpleDataStep); - const wrapper = shallowMount(NodeOutput, { + const wrapper = shallowMount(NodeOutput as any, { propsData: propsData, localVue, pinia, - provide: { transform }, + provide: { transform, workflowId: "mock-workflow" }, }); expect(wrapper.find(".multiple").exists()).toBe(false); inputTerminal.connect(outputTerminal); diff --git a/client/src/components/Workflow/Editor/NodeOutput.vue b/client/src/components/Workflow/Editor/NodeOutput.vue index e5cefe525a1f..3bcd7343e43c 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.vue +++ b/client/src/components/Workflow/Editor/NodeOutput.vue @@ -6,13 +6,13 @@ import type { UseElementBoundingReturn, UseScrollReturn } from "@vueuse/core"; import { computed, nextTick, onBeforeUnmount, type Ref, ref, toRefs, type UnwrapRef, watch } from "vue"; import type { DatatypesMapperModel } from "@/components/Datatypes/model"; -import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; +import type { XYPosition } from "@/stores/workflowEditorStateStore"; import { type OutputTerminalSource, type PostJobAction, type PostJobActions, type Step, - useWorkflowStepStore, } from "@/stores/workflowStepStore"; import { assertDefined, ensureDefined } from "@/utils/assertions"; @@ -43,8 +43,7 @@ const props = defineProps<{ }>(); const emit = defineEmits(["pan-by", "stopDragging", "onDragConnector"]); -const stateStore = useWorkflowStateStore(); -const stepStore = useWorkflowStepStore(); +const { stateStore, stepStore } = useWorkflowStores(); const icon: Ref = ref(null); const { rootOffset, output, stepId, datatypesMapper } = toRefs(props); diff --git a/client/src/components/Workflow/Editor/Recommendations.vue b/client/src/components/Workflow/Editor/Recommendations.vue index 0c43186059ef..0a2baadfa6ca 100644 --- a/client/src/components/Workflow/Editor/Recommendations.vue +++ b/client/src/components/Workflow/Editor/Recommendations.vue @@ -21,7 +21,7 @@ import LoadingSpan from "components/LoadingSpan"; import _l from "utils/localization"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; import { getToolPredictions } from "./modules/services"; import { getCompatibleRecommendations } from "./modules/utilities"; @@ -41,7 +41,7 @@ export default { }, }, setup() { - const stepStore = useWorkflowStepStore(); + const { stepStore } = useWorkflowStores(); return { stepStore }; }, data() { diff --git a/client/src/components/Workflow/Editor/SVGConnection.vue b/client/src/components/Workflow/Editor/SVGConnection.vue index 17120890ba66..2b84f4ec56b1 100644 --- a/client/src/components/Workflow/Editor/SVGConnection.vue +++ b/client/src/components/Workflow/Editor/SVGConnection.vue @@ -2,10 +2,9 @@ import { curveBasis, line } from "d3"; import { computed, type PropType } from "vue"; +import { useWorkflowStores } from "@/composables/workflowStores"; import { type Connection, getConnectionId } from "@/stores/workflowConnectionStore"; -import { useConnectionStore } from "@/stores/workflowConnectionStore"; -import { type TerminalPosition, useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; +import type { TerminalPosition } from "@/stores/workflowEditorStateStore"; const props = defineProps({ id: String, @@ -23,9 +22,7 @@ const ribbonMargin = 4; const curve = line().curve(curveBasis); -const stateStore = useWorkflowStateStore(); -const connectionStore = useConnectionStore(); -const stepStore = useWorkflowStepStore(); +const { connectionStore, stateStore, stepStore } = useWorkflowStores(); const outputPos = computed(() => { if (props.terminalPosition) { diff --git a/client/src/components/Workflow/Editor/WorkflowEdges.vue b/client/src/components/Workflow/Editor/WorkflowEdges.vue index f7a070a341a6..be9ff1d89b34 100644 --- a/client/src/components/Workflow/Editor/WorkflowEdges.vue +++ b/client/src/components/Workflow/Editor/WorkflowEdges.vue @@ -2,7 +2,8 @@ import { storeToRefs } from "pinia"; import { computed, type Ref } from "vue"; -import { type Connection, type OutputTerminal, useConnectionStore } from "@/stores/workflowConnectionStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; +import type { Connection, OutputTerminal } from "@/stores/workflowConnectionStore"; import type { TerminalPosition } from "@/stores/workflowEditorStateStore"; import type { OutputTerminals } from "./modules/terminals"; @@ -15,7 +16,7 @@ const props = defineProps<{ transform: { x: number; y: number; k: number }; }>(); -const connectionStore = useConnectionStore(); +const { connectionStore } = useWorkflowStores(); const { connections } = storeToRefs(connectionStore); const draggingConnection: Ref<[Connection, TerminalPosition] | null> = computed(() => { diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index 15d007fcd123..be91a8b56953 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -51,9 +51,9 @@ import { storeToRefs } from "pinia"; import { computed, type PropType, provide, reactive, type Ref, ref, watch, watchEffect } from "vue"; import { DatatypesMapperModel } from "@/components/Datatypes/model"; +import { useWorkflowStores } from "@/composables/workflowStores"; import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore"; -import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; -import { type Step, useWorkflowStepStore } from "@/stores/workflowStepStore"; +import type { Step } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; import { useD3Zoom } from "./composables/d3Zoom"; @@ -75,8 +75,7 @@ const props = defineProps({ scrollToId: { type: null as unknown as PropType, default: null }, }); -const stateStore = useWorkflowStateStore(); -const stepStore = useWorkflowStepStore(); +const { stateStore, stepStore } = useWorkflowStores(); const { scale, activeNodeId, draggingPosition, draggingTerminal } = storeToRefs(stateStore); const canvas: Ref = ref(null); diff --git a/client/src/components/Workflow/Editor/WorkflowMinimap.vue b/client/src/components/Workflow/Editor/WorkflowMinimap.vue index ef17b40c14fe..1a47bd7a0c38 100644 --- a/client/src/components/Workflow/Editor/WorkflowMinimap.vue +++ b/client/src/components/Workflow/Editor/WorkflowMinimap.vue @@ -5,7 +5,7 @@ import { computed, onMounted, ref, unref, watch } from "vue"; import { useAnimationFrame } from "@/composables/sensors/animationFrame"; import { useAnimationFrameThrottle } from "@/composables/throttle"; -import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; +import { useWorkflowStores } from "@/composables/workflowStores"; import type { Step, Steps } from "@/stores/workflowStepStore"; import { AxisAlignedBoundingBox, Transform } from "./modules/geometry"; @@ -21,7 +21,7 @@ const emit = defineEmits<{ (e: "moveTo", position: { x: number; y: number }): void; }>(); -const stateStore = useWorkflowStateStore(); +const { stateStore } = useWorkflowStores(); /** reference to the main canvas element */ const canvas: Ref = ref(null); diff --git a/client/src/components/Workflow/Editor/composables/useTerminal.ts b/client/src/components/Workflow/Editor/composables/useTerminal.ts index fad0aee5b2f8..482c9dfcbe0e 100644 --- a/client/src/components/Workflow/Editor/composables/useTerminal.ts +++ b/client/src/components/Workflow/Editor/composables/useTerminal.ts @@ -2,8 +2,8 @@ import { computed, type Ref, ref, watch } from "vue"; import type { DatatypesMapperModel } from "@/components/Datatypes/model"; import { terminalFactory } from "@/components/Workflow/Editor/modules/terminals"; +import { useWorkflowStores } from "@/composables/workflowStores"; import type { Step, TerminalSource } from "@/stores/workflowStepStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; export function useTerminal( stepId: Ref, @@ -11,7 +11,7 @@ export function useTerminal( datatypesMapper: Ref ) { const terminal: Ref | null> = ref(null); - const stepStore = useWorkflowStepStore(); + const { connectionStore, stepStore } = useWorkflowStores(); const step = computed(() => stepStore.getStep(stepId.value)); const isMappedOver = computed(() => stepStore.stepMapOver[stepId.value]?.isCollection ?? false); @@ -19,7 +19,13 @@ export function useTerminal( [step, terminalSource, datatypesMapper], () => { // rebuild terminal if any of the tracked dependencies change - const newTerminal = terminalFactory(stepId.value, terminalSource.value, datatypesMapper.value); + const newTerminal = terminalFactory( + stepId.value, + terminalSource.value, + datatypesMapper.value, + connectionStore, + stepStore + ); newTerminal.getInvalidConnectedTerminals(); terminal.value = newTerminal; }, diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts index 08d15ae088c3..3a4882e6a53b 100644 --- a/client/src/components/Workflow/Editor/modules/layout.ts +++ b/client/src/components/Workflow/Editor/modules/layout.ts @@ -33,9 +33,9 @@ interface NewGraph { edges: GraphEdge[]; } -export async function autoLayout(steps: { [index: string]: Step }) { - const stateStore = useWorkflowStateStore(); - const connectionStore = useConnectionStore(); +export async function autoLayout(id: string, steps: { [index: string]: Step }) { + const connectionStore = useConnectionStore(id); + const stateStore = useWorkflowStateStore(id); // Convert this to ELK compat. const newGraph: NewGraph = { diff --git a/client/src/components/Workflow/Editor/modules/linting.ts b/client/src/components/Workflow/Editor/modules/linting.ts index 169fdc13fa52..86f700e70436 100644 --- a/client/src/components/Workflow/Editor/modules/linting.ts +++ b/client/src/components/Workflow/Editor/modules/linting.ts @@ -1,6 +1,7 @@ import type { DatatypesMapperModel } from "@/components/Datatypes/model"; import type { UntypedParameters } from "@/components/Workflow/Editor/modules/parameters"; -import type { Step, Steps } from "@/stores/workflowStepStore"; +import type { useConnectionStore } from "@/stores/workflowConnectionStore"; +import type { Step, Steps, useWorkflowStepStore } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; import { terminalFactory } from "./terminals"; @@ -14,11 +15,16 @@ interface LintState { autofix?: boolean; } -export function getDisconnectedInputs(steps: Steps = {}, datatypesMapper: DatatypesMapperModel) { +export function getDisconnectedInputs( + steps: Steps = {}, + datatypesMapper: DatatypesMapperModel, + connectionStore: ReturnType, + stepStore: ReturnType +) { const inputs: LintState[] = []; Object.values(steps).forEach((step) => { step.inputs.map((inputSource) => { - const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper); + const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper, connectionStore, stepStore); if (!inputTerminal.optional && inputTerminal.connections.length === 0) { const inputLabel = inputSource.label || inputSource.name; inputs.push({ @@ -114,7 +120,13 @@ export function getUntypedParameters(untypedParameters: UntypedParameters) { return items; } -export function fixAllIssues(steps: Steps, parameters: UntypedParameters, datatypesMapper: DatatypesMapperModel) { +export function fixAllIssues( + steps: Steps, + parameters: UntypedParameters, + datatypesMapper: DatatypesMapperModel, + connectionStore: ReturnType, + stepStore: ReturnType +) { const actions = []; const untypedParameters = getUntypedParameters(parameters); for (const untypedParameter of untypedParameters) { @@ -122,7 +134,7 @@ export function fixAllIssues(steps: Steps, parameters: UntypedParameters, dataty actions.push(fixUntypedParameter(untypedParameter)); } } - const disconnectedInputs = getDisconnectedInputs(steps, datatypesMapper); + const disconnectedInputs = getDisconnectedInputs(steps, datatypesMapper, connectionStore, stepStore); for (const disconnectedInput of disconnectedInputs) { if (disconnectedInput.autofix) { actions.push(fixDisconnectedInput(disconnectedInput)); diff --git a/client/src/components/Workflow/Editor/modules/model.ts b/client/src/components/Workflow/Editor/modules/model.ts index 75f38b690160..36c5eff4e1ad 100644 --- a/client/src/components/Workflow/Editor/modules/model.ts +++ b/client/src/components/Workflow/Editor/modules/model.ts @@ -10,8 +10,21 @@ interface Workflow { steps: Steps; } -export async function fromSimple(data: Workflow, appendData = false, defaultPosition = { top: 0, left: 0 }) { - const stepStore = useWorkflowStepStore(); +/** + * Loads a workflow into the editor + * + * @param id ID of workflow to load data *into* + * @param data Workflow data to load from + * @param appendData if true appends data to current workflow, making sure to create new uuids + * @param defaultPosition where to position workflow in the editor + */ +export async function fromSimple( + id: string, + data: Workflow, + appendData = false, + defaultPosition = { top: 0, left: 0 } +) { + const stepStore = useWorkflowStepStore(id); const stepIdOffset = stepStore.getStepIndex + 1; Object.values(data.steps).forEach((step) => { // If workflow being copied into another, wipe UUID and let diff --git a/client/src/components/Workflow/Editor/modules/terminals.test.ts b/client/src/components/Workflow/Editor/modules/terminals.test.ts index b425e63b9774..1c49c1513aa6 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.test.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.test.ts @@ -24,15 +24,31 @@ import { function setupAdvanced() { const terminals: { [index: string]: { [index: string]: ReturnType } } = {}; + + const connectionStore = useConnectionStore("mock-workflow"); + const stepStore = useWorkflowStepStore("mock-workflow"); + Object.values(advancedSteps).map((step) => { const stepLabel = step.label; if (stepLabel) { terminals[stepLabel] = {}; step.inputs?.map((input) => { - terminals[stepLabel]![input.name] = terminalFactory(step.id, input, testDatatypesMapper); + terminals[stepLabel]![input.name] = terminalFactory( + step.id, + input, + testDatatypesMapper, + connectionStore, + stepStore + ); }); step.outputs?.map((output) => { - terminals[stepLabel]![output.name] = terminalFactory(step.id, output, testDatatypesMapper); + terminals[stepLabel]![output.name] = terminalFactory( + step.id, + output, + testDatatypesMapper, + connectionStore, + stepStore + ); }); } }); @@ -42,12 +58,16 @@ function setupAdvanced() { function rebuildTerminal>(terminal: T): T { let terminalSource: TerminalSource; const step = terminal.stepStore.getStep(terminal.stepId); + + const connectionStore = useConnectionStore("mock-workflow"); + const stepStore = useWorkflowStepStore("mock-workflow"); + if (terminal.terminalType === "input") { terminalSource = step!.inputs.find((input) => input.name == terminal.name)!; } else { terminalSource = step!.outputs.find((output) => output.name == terminal.name)!; } - return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper) as T; + return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper, connectionStore, stepStore) as T; } describe("terminalFactory", () => { @@ -83,7 +103,10 @@ describe("terminalFactory", () => { expect(terminals["filter_failed"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); }); it("throws error on invalid terminalSource", () => { - const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper); + const connectionStore = useConnectionStore("mock-workflow"); + const stepStore = useWorkflowStepStore("mock-workflow"); + + const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper, connectionStore, stepStore); expect(invalidFactory).toThrow(); }); }); @@ -95,8 +118,8 @@ describe("canAccept", () => { beforeEach(() => { setActivePinia(createPinia()); terminals = setupAdvanced(); - stepStore = useWorkflowStepStore(); - connectionStore = useConnectionStore(); + stepStore = useWorkflowStepStore("mock-workflow"); + connectionStore = useConnectionStore("mock-workflow"); Object.values(JSON.parse(JSON.stringify(advancedSteps)) as Steps).map((step) => { stepStore.addStep(step); }); @@ -542,18 +565,30 @@ describe("Input terminal", () => { let terminals: { [index: number]: { [index: string]: ReturnType } }; beforeEach(() => { setActivePinia(createPinia()); - stepStore = useWorkflowStepStore(); - connectionStore = useConnectionStore(); + stepStore = useWorkflowStepStore("mock-workflow"); + connectionStore = useConnectionStore("mock-workflow"); terminals = {}; Object.values(simpleSteps).map((step) => { stepStore.addStep(step); terminals[step.id] = {}; const stepTerminals = terminals[step.id]!; step.inputs?.map((input) => { - stepTerminals[input.name] = terminalFactory(step.id, input, testDatatypesMapper); + stepTerminals[input.name] = terminalFactory( + step.id, + input, + testDatatypesMapper, + connectionStore, + stepStore + ); }); step.outputs?.map((output) => { - stepTerminals[output.name] = terminalFactory(step.id, output, testDatatypesMapper); + stepTerminals[output.name] = terminalFactory( + step.id, + output, + testDatatypesMapper, + connectionStore, + stepStore + ); }); }); }); diff --git a/client/src/components/Workflow/Editor/modules/terminals.ts b/client/src/components/Workflow/Editor/modules/terminals.ts index 2821748a5df6..662954a2567f 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.ts @@ -5,7 +5,7 @@ import { type Connection, type ConnectionId, getConnectionId, - useConnectionStore, + type useConnectionStore, } from "@/stores/workflowConnectionStore"; import type { CollectionOutput, @@ -15,8 +15,8 @@ import type { ParameterOutput, ParameterStepInput, TerminalSource, + useWorkflowStepStore, } from "@/stores/workflowStepStore"; -import { useWorkflowStepStore } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; import { @@ -39,6 +39,8 @@ interface BaseTerminalArgs { name: string; stepId: number; datatypesMapper: DatatypesMapperModel; + connectionStore: ReturnType; + stepStore: ReturnType; } interface InputTerminalInputs { @@ -64,8 +66,8 @@ class Terminal extends EventEmitter { constructor(attr: BaseTerminalArgs) { super(); - this.connectionStore = useConnectionStore(); - this.stepStore = useWorkflowStepStore(); + this.connectionStore = attr.connectionStore; + this.stepStore = attr.stepStore; this.stepId = attr.stepId; this.name = attr.name; this.multiple = false; @@ -244,12 +246,24 @@ class BaseInputTerminal extends Terminal { // step must have an output, since it is or was connected to this step const terminalSource = step.outputs[0]; if (terminalSource) { - const terminal = terminalFactory(step.id, terminalSource, this.datatypesMapper); + const terminal = terminalFactory( + step.id, + terminalSource, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); // drop mapping restrictions terminal.resetMappingIfNeeded(); // re-establish map over through inputs step.inputs.forEach((input) => { - terminalFactory(step.id, input, this.datatypesMapper).getStepMapOver(); + terminalFactory( + step.id, + input, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ).getStepMapOver(); }); } } else { @@ -356,6 +370,8 @@ class BaseInputTerminal extends Terminal { name: connection.output.name, valid: false, datatypesMapper: this.datatypesMapper, + connectionStore: this.connectionStore, + stepStore: this.stepStore, }); } let terminalSource = outputStep.outputs.find((output) => output.name === connection.output.name); @@ -367,6 +383,8 @@ class BaseInputTerminal extends Terminal { name: connection.output.name, valid: false, datatypesMapper: this.datatypesMapper, + connectionStore: this.connectionStore, + stepStore: this.stepStore, }); } const postJobActionKey = `ChangeDatatypeAction${connection.output.name}`; @@ -383,7 +401,13 @@ class BaseInputTerminal extends Terminal { }; } - return terminalFactory(outputStep.id, terminalSource, this.datatypesMapper); + return terminalFactory( + outputStep.id, + terminalSource, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); }); } @@ -658,9 +682,17 @@ class BaseOutputTerminal extends Terminal { optional: false, multiple: false, }, + connectionStore: this.connectionStore, + stepStore: this.stepStore, }); } - return terminalFactory(inputStep.id, terminalSource, this.datatypesMapper); + return terminalFactory( + inputStep.id, + terminalSource, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); }); } @@ -689,7 +721,13 @@ class BaseOutputTerminal extends Terminal { const validInputTerminals: InputTerminals[] = []; Object.values(this.stepStore.steps).map((step) => { step.inputs?.forEach((input) => { - const inputTerminal = terminalFactory(step.id, input, this.datatypesMapper); + const inputTerminal = terminalFactory( + step.id, + input, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); if (inputTerminal.canAccept(this).canAccept) { validInputTerminals.push(inputTerminal); } @@ -738,8 +776,20 @@ export class OutputCollectionTerminal extends BaseOutputTerminal { const stepOutput = outputStep.outputs.find((output) => output.name == connection.output.name); const stepInput = inputStep.inputs.find((input) => input.name === this.collectionTypeSource); if (stepInput && stepOutput) { - const outputTerminal = terminalFactory(connection.output.stepId, stepOutput, this.datatypesMapper); - const inputTerminal = terminalFactory(connection.output.stepId, stepInput, this.datatypesMapper); + const outputTerminal = terminalFactory( + connection.output.stepId, + stepOutput, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); + const inputTerminal = terminalFactory( + connection.output.stepId, + stepInput, + this.datatypesMapper, + this.connectionStore, + this.stepStore + ); // otherCollectionType is the mapped over output collection as it would appear at the input terminal const otherCollectionType = inputTerminal._otherCollectionType(outputTerminal); // we need to find which of the possible input collection types is connected @@ -885,7 +935,9 @@ type TerminalOf = T extends InvalidInputTerm export function terminalFactory( stepId: number, terminalSource: T, - datatypesMapper: DatatypesMapperModel + datatypesMapper: DatatypesMapperModel, + connectionStore: ReturnType, + stepStore: ReturnType ): TerminalOf { if ("input_type" in terminalSource) { const terminalArgs = { @@ -893,6 +945,8 @@ export function terminalFactory( input_type: terminalSource.input_type, name: terminalSource.name, stepId: stepId, + connectionStore, + stepStore, }; if ("valid" in terminalSource) { return new InvalidInputTerminal({ @@ -940,6 +994,8 @@ export function terminalFactory( optional: terminalSource.optional, stepId: stepId, datatypesMapper: datatypesMapper, + connectionStore, + stepStore, }; if (isOutputParameterArg(terminalSource)) { return new OutputParameterTerminal({ diff --git a/client/src/composables/workflowStores.ts b/client/src/composables/workflowStores.ts new file mode 100644 index 000000000000..40893840e5b0 --- /dev/null +++ b/client/src/composables/workflowStores.ts @@ -0,0 +1,63 @@ +import { inject, onScopeDispose, provide } from "vue"; + +import { useConnectionStore } from "@/stores/workflowConnectionStore"; +import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; +import { useWorkflowStepStore } from "@/stores/workflowStepStore"; + +/** + * Creates stores scoped to a specific workflowId, and manages their lifetime. + * In child components, use `useWorkflowStores` instead. + * + * Provides `workflowId` to all child components. + * + * @param workflowId the workflow to scope to + * @returns workflow Stores + */ +export function provideScopedWorkflowStores(workflowId: string) { + provide("workflowId", workflowId); + + const connectionStore = useConnectionStore(workflowId); + const stateStore = useWorkflowStateStore(workflowId); + const stepStore = useWorkflowStepStore(workflowId); + + onScopeDispose(() => { + connectionStore.$dispose(); + stateStore.$dispose(); + stepStore.$dispose(); + }); + + return { + connectionStore, + stateStore, + stepStore, + }; +} + +/** + * Uses all workflow related stores scoped to the workflow defined by a parent component. + * Does not manage lifetime. + * + * `provideScopedWorkflowStores` needs to be called by a parent component, + * or this composable will throw an error. + * + * @returns workflow stores + */ +export function useWorkflowStores() { + const workflowId = inject("workflowId"); + + if (typeof workflowId !== "string") { + throw new Error( + "Workflow ID not provided by parent component. Use `setupWorkflowStores` on a parent component." + ); + } + + const connectionStore = useConnectionStore(workflowId); + const stateStore = useWorkflowStateStore(workflowId); + const stepStore = useWorkflowStepStore(workflowId); + + return { + connectionStore, + stateStore, + stepStore, + }; +} diff --git a/client/src/stores/workflowConnectionStore.test.ts b/client/src/stores/workflowConnectionStore.test.ts index 156fa0e9555a..e1d14da96148 100644 --- a/client/src/stores/workflowConnectionStore.test.ts +++ b/client/src/stores/workflowConnectionStore.test.ts @@ -38,25 +38,25 @@ const connection: Connection = { describe("Connection Store", () => { beforeEach(() => { setActivePinia(createPinia()); - const workflowStepStore = useWorkflowStepStore(); + const workflowStepStore = useWorkflowStepStore("mock-workflow"); workflowStepStore.addStep(workflowStepZero); workflowStepStore.addStep(workflowStepOne); }); it("adds connection", () => { - const connectionStore = useConnectionStore(); + const connectionStore = useConnectionStore("mock-workflow"); expect(connectionStore.connections.length).toBe(0); connectionStore.addConnection(connection); expect(connectionStore.connections.length).toBe(1); }); it("removes connection", () => { - const connectionStore = useConnectionStore(); + const connectionStore = useConnectionStore("mock-workflow"); connectionStore.addConnection(connection); connectionStore.removeConnection(inputTerminal); expect(connectionStore.connections.length).toBe(0); }); it("finds connections for steps", () => { - const connectionStore = useConnectionStore(); + const connectionStore = useConnectionStore("mock-workflow"); expect(connectionStore.getConnectionsForStep(0)).toStrictEqual([]); expect(connectionStore.getConnectionsForStep(1)).toStrictEqual([]); connectionStore.addConnection(connection); @@ -67,7 +67,7 @@ describe("Connection Store", () => { expect(connectionStore.getConnectionsForStep(1)).toStrictEqual([]); }); it("finds output terminals for input terminal", () => { - const connectionStore = useConnectionStore(); + const connectionStore = useConnectionStore("mock-workflow"); expect(connectionStore.getOutputTerminalsForInputTerminal(getTerminalId(connection.input))).toStrictEqual([]); connectionStore.addConnection(connection); expect(connectionStore.getOutputTerminalsForInputTerminal(getTerminalId(connection.input))).toStrictEqual([ diff --git a/client/src/stores/workflowConnectionStore.ts b/client/src/stores/workflowConnectionStore.ts index 522a86bb2b23..686cc9b4a287 100644 --- a/client/src/stores/workflowConnectionStore.ts +++ b/client/src/stores/workflowConnectionStore.ts @@ -42,82 +42,88 @@ interface TerminalToOutputTerminals { [index: string]: OutputTerminal[]; } -export const useConnectionStore = defineStore("workflowConnectionStore", { - state: (): State => ({ - connections: [] as Array>, - invalidConnections: {} as InvalidConnections, - inputTerminalToOutputTerminals: {} as TerminalToOutputTerminals, - terminalToConnection: {} as { [index: string]: Connection[] }, - stepToConnections: {} as { [index: number]: Connection[] }, - }), - getters: { - getOutputTerminalsForInputTerminal(state: State) { - return (terminalId: string): OutputTerminal[] => { - return state.inputTerminalToOutputTerminals[terminalId] || []; - }; +export const useConnectionStore = (workflowId: string) => { + if (!workflowId) { + throw new Error("WorkflowId is undefined"); + } + + return defineStore(`workflowConnectionStore${workflowId}`, { + state: (): State => ({ + connections: [] as Array>, + invalidConnections: {} as InvalidConnections, + inputTerminalToOutputTerminals: {} as TerminalToOutputTerminals, + terminalToConnection: {} as { [index: string]: Connection[] }, + stepToConnections: {} as { [index: number]: Connection[] }, + }), + getters: { + getOutputTerminalsForInputTerminal(state: State) { + return (terminalId: string): OutputTerminal[] => { + return state.inputTerminalToOutputTerminals[terminalId] || []; + }; + }, + getConnectionsForTerminal(state: State) { + return (terminalId: string): Connection[] => { + return state.terminalToConnection[terminalId] || []; + }; + }, + getConnectionsForStep(state: State) { + return (stepId: number): Connection[] => state.stepToConnections[stepId] || []; + }, }, - getConnectionsForTerminal(state: State) { - return (terminalId: string): Connection[] => { - return state.terminalToConnection[terminalId] || []; - }; - }, - getConnectionsForStep(state: State) { - return (stepId: number): Connection[] => state.stepToConnections[stepId] || []; - }, - }, - actions: { - addConnection(this, _connection: Connection) { - const connection = Object.freeze(_connection); - this.connections.push(connection); - const stepStore = useWorkflowStepStore(); - stepStore.addConnection(connection); - this.terminalToConnection = updateTerminalToConnection(this.connections); - this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections); - this.stepToConnections = updateStepToConnections(this.connections); - }, - markInvalidConnection(this: State, connectionId: string, reason: string) { - Vue.set(this.invalidConnections, connectionId, reason); - }, - dropFromInvalidConnections(this: State, connectionId: string) { - Vue.delete(this.invalidConnections, connectionId); - }, - removeConnection(this, terminal: InputTerminal | OutputTerminal | ConnectionId) { - const stepStore = useWorkflowStepStore(); - this.connections = this.connections.filter((connection) => { - const id = getConnectionId(connection); - - if (typeof terminal === "string") { - if (id === terminal) { - stepStore.removeConnection(connection); - Vue.delete(this.invalidConnections, id); - return false; - } else { - return true; - } - } else if (terminal.connectorType === "input") { - if (connection.input.stepId == terminal.stepId && connection.input.name == terminal.name) { - stepStore.removeConnection(connection); - Vue.delete(this.invalidConnections, id); - return false; - } else { - return true; - } - } else { - if (connection.output.stepId == terminal.stepId && connection.output.name == terminal.name) { - stepStore.removeConnection(connection); - Vue.delete(this.invalidConnections, id); - return false; + actions: { + addConnection(this, _connection: Connection) { + const connection = Object.freeze(_connection); + this.connections.push(connection); + const stepStore = useWorkflowStepStore(workflowId); + stepStore.addConnection(connection); + this.terminalToConnection = updateTerminalToConnection(this.connections); + this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections); + this.stepToConnections = updateStepToConnections(this.connections); + }, + markInvalidConnection(this: State, connectionId: string, reason: string) { + Vue.set(this.invalidConnections, connectionId, reason); + }, + dropFromInvalidConnections(this: State, connectionId: string) { + Vue.delete(this.invalidConnections, connectionId); + }, + removeConnection(this, terminal: InputTerminal | OutputTerminal | ConnectionId) { + const stepStore = useWorkflowStepStore(workflowId); + this.connections = this.connections.filter((connection) => { + const id = getConnectionId(connection); + + if (typeof terminal === "string") { + if (id === terminal) { + stepStore.removeConnection(connection); + Vue.delete(this.invalidConnections, id); + return false; + } else { + return true; + } + } else if (terminal.connectorType === "input") { + if (connection.input.stepId == terminal.stepId && connection.input.name == terminal.name) { + stepStore.removeConnection(connection); + Vue.delete(this.invalidConnections, id); + return false; + } else { + return true; + } } else { - return true; + if (connection.output.stepId == terminal.stepId && connection.output.name == terminal.name) { + stepStore.removeConnection(connection); + Vue.delete(this.invalidConnections, id); + return false; + } else { + return true; + } } - } - }); - this.terminalToConnection = updateTerminalToConnection(this.connections); - this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections); - this.stepToConnections = updateStepToConnections(this.connections); + }); + this.terminalToConnection = updateTerminalToConnection(this.connections); + this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections); + this.stepToConnections = updateStepToConnections(this.connections); + }, }, - }, -}); + })(); +}; function updateTerminalToTerminal(connections: Connection[]) { const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {}; diff --git a/client/src/stores/workflowEditorStateStore.ts b/client/src/stores/workflowEditorStateStore.ts index 4dcbecd3167f..b7e5f1bdfbfb 100644 --- a/client/src/stores/workflowEditorStateStore.ts +++ b/client/src/stores/workflowEditorStateStore.ts @@ -33,67 +33,69 @@ interface State { stepLoadingState: { [index: number]: { loading?: boolean; error?: string } }; } -export const useWorkflowStateStore = defineStore("workflowStateStore", { - state: (): State => ({ - inputTerminals: {}, - outputTerminals: {}, - draggingPosition: null, - draggingTerminal: null, - activeNodeId: null, - scale: 1, - stepPosition: {}, - stepLoadingState: {}, - }), - getters: { - getInputTerminalPosition(state: State) { - return (stepId: number, inputName: string) => { - return state.inputTerminals[stepId]?.[inputName] as InputTerminalPosition | undefined; - }; +export const useWorkflowStateStore = (workflowId: string) => { + return defineStore(`workflowStateStore${workflowId}`, { + state: (): State => ({ + inputTerminals: {}, + outputTerminals: {}, + draggingPosition: null, + draggingTerminal: null, + activeNodeId: null, + scale: 1, + stepPosition: {}, + stepLoadingState: {}, + }), + getters: { + getInputTerminalPosition(state: State) { + return (stepId: number, inputName: string) => { + return state.inputTerminals[stepId]?.[inputName] as InputTerminalPosition | undefined; + }; + }, + getOutputTerminalPosition(state: State) { + return (stepId: number, outputName: string) => { + return state.outputTerminals[stepId]?.[outputName] as OutputTerminalPosition | undefined; + }; + }, + getStepLoadingState(state: State) { + return (stepId: number) => state.stepLoadingState[stepId]; + }, }, - getOutputTerminalPosition(state: State) { - return (stepId: number, outputName: string) => { - return state.outputTerminals[stepId]?.[outputName] as OutputTerminalPosition | undefined; - }; - }, - getStepLoadingState(state: State) { - return (stepId: number) => state.stepLoadingState[stepId]; - }, - }, - actions: { - setInputTerminalPosition(stepId: number, inputName: string, position: InputTerminalPosition) { - if (!this.inputTerminals[stepId]) { - Vue.set(this.inputTerminals, stepId, {}); - } + actions: { + setInputTerminalPosition(stepId: number, inputName: string, position: InputTerminalPosition) { + if (!this.inputTerminals[stepId]) { + Vue.set(this.inputTerminals, stepId, {}); + } - Vue.set(this.inputTerminals[stepId]!, inputName, position); - }, - setOutputTerminalPosition(stepId: number, outputName: string, position: OutputTerminalPosition) { - if (!this.outputTerminals[stepId]) { - Vue.set(this.outputTerminals, stepId, reactive({})); - } + Vue.set(this.inputTerminals[stepId]!, inputName, position); + }, + setOutputTerminalPosition(stepId: number, outputName: string, position: OutputTerminalPosition) { + if (!this.outputTerminals[stepId]) { + Vue.set(this.outputTerminals, stepId, reactive({})); + } - Vue.set(this.outputTerminals[stepId]!, outputName, position); - }, - deleteInputTerminalPosition(stepId: number, inputName: string) { - delete this.inputTerminals[stepId]?.[inputName]; - }, - deleteOutputTerminalPosition(stepId: number, outputName: string) { - delete this.outputTerminals[stepId]?.[outputName]; - }, - setActiveNode(nodeId: number | null) { - this.activeNodeId = nodeId; - }, - setScale(scale: number) { - this.scale = scale; - }, - setStepPosition(stepId: number, position: UnwrapRef) { - Vue.set(this.stepPosition, stepId, position); - }, - deleteStepPosition(stepId: number) { - delete this.stepPosition[stepId]; - }, - setLoadingState(stepId: number, loading: boolean, error: string | undefined) { - Vue.set(this.stepLoadingState, stepId, { loading, error }); + Vue.set(this.outputTerminals[stepId]!, outputName, position); + }, + deleteInputTerminalPosition(stepId: number, inputName: string) { + delete this.inputTerminals[stepId]?.[inputName]; + }, + deleteOutputTerminalPosition(stepId: number, outputName: string) { + delete this.outputTerminals[stepId]?.[outputName]; + }, + setActiveNode(nodeId: number | null) { + this.activeNodeId = nodeId; + }, + setScale(scale: number) { + this.scale = scale; + }, + setStepPosition(stepId: number, position: UnwrapRef) { + Vue.set(this.stepPosition, stepId, position); + }, + deleteStepPosition(stepId: number) { + delete this.stepPosition[stepId]; + }, + setLoadingState(stepId: number, loading: boolean, error: string | undefined) { + Vue.set(this.stepLoadingState, stepId, { loading, error }); + }, }, - }, -}); + })(); +}; diff --git a/client/src/stores/workflowStepStore.test.ts b/client/src/stores/workflowStepStore.test.ts index a3fbe8f1559f..9d9258b96550 100644 --- a/client/src/stores/workflowStepStore.test.ts +++ b/client/src/stores/workflowStepStore.test.ts @@ -32,29 +32,29 @@ describe("Connection Store", () => { }); it("adds step", () => { - const stepStore = useWorkflowStepStore(); + const stepStore = useWorkflowStepStore("mock-workflow"); expect(stepStore.steps).toStrictEqual({}); stepStore.addStep(workflowStepZero); expect(stepStore.getStep(0)).toStrictEqual(workflowStepZero); expect(workflowStepZero.id).toBe(0); }); it("removes step", () => { - const stepStore = useWorkflowStepStore(); + const stepStore = useWorkflowStepStore("mock-workflow"); const addedStep = stepStore.addStep(workflowStepZero); expect(addedStep.id).toBe(0); stepStore.removeStep(addedStep.id); expect(stepStore.getStep(0)).toBe(undefined); }); it("creates connection if step has connection", () => { - const stepStore = useWorkflowStepStore(); - const connectionStore = useConnectionStore(); + const stepStore = useWorkflowStepStore("mock-workflow"); + const connectionStore = useConnectionStore("mock-workflow"); stepStore.addStep(workflowStepZero); stepStore.addStep(workflowStepOne); expect(connectionStore.connections.length).toBe(1); }); it("removes connection if step has connection", () => { - const stepStore = useWorkflowStepStore(); - const connectionStore = useConnectionStore(); + const stepStore = useWorkflowStepStore("mock-workflow"); + const connectionStore = useConnectionStore("mock-workflow"); stepStore.addStep(workflowStepZero); const stepOne = stepStore.addStep(workflowStepOne); expect(connectionStore.connections.length).toBe(1); diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index 8457cf5c98b3..471ed7bda8b7 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -142,165 +142,170 @@ interface StepInputMapOver { [index: number]: { [index: string]: CollectionTypeDescriptor }; } -export const useWorkflowStepStore = defineStore("workflowStepStore", { - state: (): State => ({ - steps: {} as Steps, - stepMapOver: {} as { [index: number]: CollectionTypeDescriptor }, - stepInputMapOver: {} as StepInputMapOver, - stepIndex: -1, - stepExtraInputs: {} as { [index: number]: InputTerminalSource[] }, - }), - getters: { - getStep(state: State) { - return (stepId: number): Step | undefined => { - return state.steps[stepId.toString()]; - }; - }, - getStepExtraInputs(state: State) { - return (stepId: number) => this.stepExtraInputs[stepId] || []; - }, - getStepIndex(state: State) { - return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex); - }, - hasActiveOutputs(state: State) { - return Boolean(Object.values(state.steps).find((step) => step.workflow_outputs?.length)); +export const useWorkflowStepStore = (workflowId: string) => { + return defineStore(`workflowStepStore${workflowId}`, { + state: (): State => ({ + steps: {} as Steps, + stepMapOver: {} as { [index: number]: CollectionTypeDescriptor }, + stepInputMapOver: {} as StepInputMapOver, + stepIndex: -1, + stepExtraInputs: {} as { [index: number]: InputTerminalSource[] }, + }), + getters: { + getStep(state: State) { + return (stepId: number): Step | undefined => { + return state.steps[stepId.toString()]; + }; + }, + getStepExtraInputs(state: State) { + return (stepId: number) => this.stepExtraInputs[stepId] || []; + }, + getStepIndex(state: State) { + return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex); + }, + hasActiveOutputs(state: State) { + return Boolean(Object.values(state.steps).find((step) => step.workflow_outputs?.length)); + }, + workflowOutputs(state: State) { + const workflowOutputs: WorkflowOutputs = {}; + Object.values(state.steps).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; + }, }, - workflowOutputs(state: State) { - const workflowOutputs: WorkflowOutputs = {}; - Object.values(state.steps).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, - }; - } - }); + actions: { + addStep(newStep: NewStep): Step { + const stepId = newStep.id ? newStep.id : this.getStepIndex + 1; + const step = Object.freeze({ ...newStep, id: stepId } as Step); + Vue.set(this.steps, stepId.toString(), step); + const connectionStore = useConnectionStore(workflowId); + stepToConnections(step).map((connection) => connectionStore.addConnection(connection)); + this.stepExtraInputs[step.id] = getStepExtraInputs(step); + return step; + }, + 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 this.addStep(stepData); + }, + updateStep(this: State, step: Step) { + const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) => + step.outputs.find((output) => workflowOutput.output_name == output.name) + ); + this.steps[step.id.toString()] = Object.freeze({ ...step, workflow_outputs }); + this.stepExtraInputs[step.id] = getStepExtraInputs(step); + }, + changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) { + Vue.set(this.stepMapOver, stepId, mapOver); + }, + resetStepInputMapOver(stepId: number) { + Vue.set(this.stepInputMapOver, stepId, {}); + }, + changeStepInputMapOver(stepId: number, inputName: string, mapOver: CollectionTypeDescriptor) { + if (this.stepInputMapOver[stepId]) { + Vue.set(this.stepInputMapOver[stepId]!, inputName, mapOver); + } else { + Vue.set(this.stepInputMapOver, stepId, { [inputName]: mapOver }); } - }); - return workflowOutputs; - }, - }, - actions: { - addStep(newStep: NewStep): Step { - const stepId = newStep.id ? newStep.id : this.getStepIndex + 1; - const step = Object.freeze({ ...newStep, id: stepId } as Step); - Vue.set(this.steps, stepId.toString(), step); - const connectionStore = useConnectionStore(); - stepToConnections(step).map((connection) => connectionStore.addConnection(connection)); - this.stepExtraInputs[step.id] = getStepExtraInputs(step); - return step; - }, - 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 this.addStep(stepData); - }, - updateStep(this: State, step: Step) { - const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) => - step.outputs.find((output) => workflowOutput.output_name == output.name) - ); - this.steps[step.id.toString()] = Object.freeze({ ...step, workflow_outputs }); - this.stepExtraInputs[step.id] = getStepExtraInputs(step); - }, - changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) { - Vue.set(this.stepMapOver, stepId, mapOver); - }, - resetStepInputMapOver(stepId: number) { - Vue.set(this.stepInputMapOver, stepId, {}); - }, - changeStepInputMapOver(stepId: number, inputName: string, mapOver: CollectionTypeDescriptor) { - if (this.stepInputMapOver[stepId]) { - Vue.set(this.stepInputMapOver[stepId]!, inputName, mapOver); - } else { - Vue.set(this.stepInputMapOver, stepId, { [inputName]: mapOver }); - } - }, - addConnection(connection: Connection) { - const inputStep = this.getStep(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]; + }, + addConnection(connection: Connection) { + const inputStep = this.getStep(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]; } - inputConnection = inputConnection.filter( - (connection) => - !(connection.id === connectionLink.id && connection.output_name === connectionLink.output_name) + 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 + ), + }, + }; + this.updateStep(updatedStep); + }, + removeConnection(connection: Connection) { + const inputStep = this.getStep(connection.input.stepId); + assertDefined( + inputStep, + `Failed to remove connection, because step with id ${connection.input.stepId} is undefined` ); - 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 - ), - }, - }; - this.updateStep(updatedStep); - }, - removeConnection(connection: Connection) { - const inputStep = this.getStep(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 (this.getStepExtraInputs(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) - ); + const inputConnections = inputStep.input_connections[connection.input.name]; + if (this.getStepExtraInputs(inputStep.id).find((input) => connection.input.name === input.name)) { + inputStep.input_connections[connection.input.name] = undefined; } else { - Vue.delete(inputStep.input_connections, connection.input.name); + 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 { + Vue.delete(inputStep.input_connections, connection.input.name); + } } - } - this.updateStep(inputStep); - }, - removeStep(this: State, stepId: number) { - const connectionStore = useConnectionStore(); - connectionStore - .getConnectionsForStep(stepId) - .forEach((connection) => connectionStore.removeConnection(getConnectionId(connection))); - Vue.delete(this.steps, stepId.toString()); - Vue.delete(this.stepExtraInputs, stepId); + this.updateStep(inputStep); + }, + removeStep(this: State, stepId: number) { + const connectionStore = useConnectionStore(workflowId); + connectionStore + .getConnectionsForStep(stepId) + .forEach((connection) => connectionStore.removeConnection(getConnectionId(connection))); + Vue.delete(this.steps, stepId.toString()); + Vue.delete(this.stepExtraInputs, stepId); + }, }, - }, -}); + })(); +}; export function stepToConnections(step: Step): Connection[] { const connections: Connection[] = [];