Merge pull request #16523 from ElectronicBlueberry/workflow-store-scope

Scope Workflow Stores to Workflow ID
This commit is contained in:
Marius van den Beek
2023-08-08 14:15:33 +02:00
committed by GitHub
32 changed files with 643 additions and 408 deletions
@@ -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<InputObject[]> = computed(() => {
@@ -34,6 +34,9 @@ describe("FormDefault", () => {
},
localVue,
pinia: createTestingPinia(),
provide: {
workflowId: "mock-workflow",
},
});
});
@@ -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) {
@@ -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);
});
@@ -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<string | undefined> = ref(undefined);
const id = computed(() => `__label__${props.name}`);
@@ -48,6 +48,7 @@ describe("FormTool", () => {
ToolFooter: { template: "<div>tool-footer</div>" },
},
pinia: createTestingPinia(),
provide: { workflowId: "mock-workflow" },
});
}
@@ -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 {
+12 -14
View File
@@ -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) => {
@@ -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);
+13 -6
View File
@@ -6,7 +6,7 @@
Best Practices Review
</div>
<div v-if="showRefactor">
<a href="#" @click="onRefactor"> Try to automatically fix issues. </a>
<a class="refactor-button" href="#" @click="onRefactor"> Try to automatically fix issues. </a>
</div>
</template>
<b-card-body>
@@ -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);
},
},
@@ -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 ...
@@ -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<HTMLElement | null> = 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,
@@ -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);
@@ -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<typeof createPinia>;
let stepStore: ReturnType<typeof useWorkflowStepStore>;
let connectionStore: ReturnType<typeof useConnectionStore>;
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);
@@ -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<HTMLElement | null> = ref(null);
const { rootOffset, output, stepId, datatypesMapper } = toRefs(props);
@@ -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() {
@@ -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) {
@@ -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(() => {
@@ -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<number | null>, default: null },
});
const stateStore = useWorkflowStateStore();
const stepStore = useWorkflowStepStore();
const { stateStore, stepStore } = useWorkflowStores();
const { scale, activeNodeId, draggingPosition, draggingTerminal } = storeToRefs(stateStore);
const canvas: Ref<HTMLElement | null> = ref(null);
@@ -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<HTMLCanvasElement | null> = ref(null);
@@ -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<Step["id"]>,
@@ -11,7 +11,7 @@ export function useTerminal(
datatypesMapper: Ref<DatatypesMapperModel>
) {
const terminal: Ref<ReturnType<typeof terminalFactory> | 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;
},
@@ -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 = {
@@ -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<typeof useConnectionStore>,
stepStore: ReturnType<typeof useWorkflowStepStore>
) {
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<typeof useConnectionStore>,
stepStore: ReturnType<typeof useWorkflowStepStore>
) {
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));
@@ -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
@@ -24,15 +24,31 @@ import {
function setupAdvanced() {
const terminals: { [index: string]: { [index: string]: ReturnType<typeof terminalFactory> } } = {};
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<T extends ReturnType<typeof terminalFactory>>(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<typeof terminalFactory> } };
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
);
});
});
});
@@ -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<typeof useConnectionStore>;
stepStore: ReturnType<typeof useWorkflowStepStore>;
}
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 TerminalSourceAndInvalid> = T extends InvalidInputTerm
export function terminalFactory<T extends TerminalSourceAndInvalid>(
stepId: number,
terminalSource: T,
datatypesMapper: DatatypesMapperModel
datatypesMapper: DatatypesMapperModel,
connectionStore: ReturnType<typeof useConnectionStore>,
stepStore: ReturnType<typeof useWorkflowStepStore>
): TerminalOf<T> {
if ("input_type" in terminalSource) {
const terminalArgs = {
@@ -893,6 +945,8 @@ export function terminalFactory<T extends TerminalSourceAndInvalid>(
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<T extends TerminalSourceAndInvalid>(
optional: terminalSource.optional,
stepId: stepId,
datatypesMapper: datatypesMapper,
connectionStore,
stepStore,
};
if (isOutputParameterArg(terminalSource)) {
return new OutputParameterTerminal({
+63
View File
@@ -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,
};
}
@@ -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([
+80 -74
View File
@@ -42,82 +42,88 @@ interface TerminalToOutputTerminals {
[index: string]: OutputTerminal[];
}
export const useConnectionStore = defineStore("workflowConnectionStore", {
state: (): State => ({
connections: [] as Array<Readonly<Connection>>,
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] || [];
},
},
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);
export const useConnectionStore = (workflowId: string) => {
if (!workflowId) {
throw new Error("WorkflowId is undefined");
}
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;
} else {
return true;
}
}
});
this.terminalToConnection = updateTerminalToConnection(this.connections);
this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
this.stepToConnections = updateStepToConnections(this.connections);
return defineStore(`workflowConnectionStore${workflowId}`, {
state: (): State => ({
connections: [] as Array<Readonly<Connection>>,
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] || [];
},
},
},
});
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 {
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);
},
},
})();
};
function updateTerminalToTerminal(connections: Connection[]) {
const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
+62 -60
View File
@@ -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);
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<UseElementBoundingReturn>) {
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 });
},
},
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<UseElementBoundingReturn>) {
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 });
},
},
});
})();
};
+6 -6
View File
@@ -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);
+159 -154
View File
@@ -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()];
};
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;
},
},
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;
},
},
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];
}
inputConnection = inputConnection.filter(
(connection) =>
!(connection.id === connectionLink.id && connection.output_name === connectionLink.output_name)
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)
);
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)
);
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.delete(inputStep.input_connections, connection.input.name);
Vue.set(this.stepInputMapOver, stepId, { [inputName]: mapOver });
}
}
this.updateStep(inputStep);
},
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];
}
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)
);
} else {
Vue.delete(inputStep.input_connections, connection.input.name);
}
}
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);
},
},
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);
},
},
});
})();
};
export function stepToConnections(step: Step): Connection[] {
const connections: Connection[] = [];