From 741e93e73b7aae06ff4d7f0bf3008df3430f4c88 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:12:39 +0100 Subject: [PATCH 01/49] add undo-redo store --- client/src/stores/undoRedoStore/index.ts | 54 +++++++++++++++++++ .../stores/undoRedoStore/undoRedoAction.ts | 22 ++++++++ 2 files changed, 76 insertions(+) create mode 100644 client/src/stores/undoRedoStore/index.ts create mode 100644 client/src/stores/undoRedoStore/undoRedoAction.ts diff --git a/client/src/stores/undoRedoStore/index.ts b/client/src/stores/undoRedoStore/index.ts new file mode 100644 index 000000000000..0dfb6e046e0b --- /dev/null +++ b/client/src/stores/undoRedoStore/index.ts @@ -0,0 +1,54 @@ +import { ref } from "vue"; + +import { defineScopedStore } from "@/stores/scopedStore"; + +import { UndoRedoAction } from "./undoRedoAction"; + +export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { + const undoActionStack = ref([]); + const redoActionStack = ref([]); + const maxUndoActions = ref(100); + + function undo() { + const action = undoActionStack.value.pop(); + + if (action) { + action.undo(); + redoActionStack.value.push(action); + } + } + + function redo() { + const action = redoActionStack.value.pop(); + + if (action) { + action.redo(); + undoActionStack.value.push(action); + } + } + + function applyAction(action: UndoRedoAction) { + action.run(); + clearRedoStack(); + undoActionStack.value.push(action); + + while (undoActionStack.value.length > maxUndoActions.value && undoActionStack.value.length > 0) { + const action = undoActionStack.value.shift(); + action?.destroy(); + } + } + + function clearRedoStack() { + redoActionStack.value.forEach((action) => action.destroy()); + redoActionStack.value = []; + } + + return { + undoActionStack, + redoActionStack, + maxUndoActions, + undo, + redo, + applyAction, + }; +}); diff --git a/client/src/stores/undoRedoStore/undoRedoAction.ts b/client/src/stores/undoRedoStore/undoRedoAction.ts new file mode 100644 index 000000000000..ad2899c412d7 --- /dev/null +++ b/client/src/stores/undoRedoStore/undoRedoAction.ts @@ -0,0 +1,22 @@ +export class UndoRedoAction { + onRun?: () => void; + onUndo?: () => void; + onRedo?: () => void; + onDestroy?: () => void; + + run() { + this.onRun ? this.onRun() : null; + } + + undo() { + this.onUndo ? this.onUndo() : null; + } + + redo() { + this.onRedo ? this.onRedo() : this.run(); + } + + destroy() { + this.onDestroy ? this.onDestroy() : null; + } +} From b6ae7a87cafa41cf3b04e6331a10fedcda0ccd6e Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:05:37 +0100 Subject: [PATCH 02/49] undo-redo adding comments --- .../src/components/Workflow/Editor/Index.vue | 9 ++++- .../Workflow/Editor/Tools/ToolBar.vue | 4 +-- .../Workflow/Editor/Tools/useToolLogic.ts | 33 ++++++++++++++++--- client/src/composables/workflowStores.ts | 6 ++++ client/src/stores/undoRedoStore/index.ts | 8 +++-- .../stores/undoRedoStore/undoRedoAction.ts | 32 +++++++++++++----- 6 files changed, 74 insertions(+), 18 deletions(-) diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 64502b77a927..284e94d96448 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -168,6 +168,7 @@ diff --git a/client/src/stores/undoRedoStore/index.ts b/client/src/stores/undoRedoStore/index.ts index 66523b1d5e2b..173de34744a8 100644 --- a/client/src/stores/undoRedoStore/index.ts +++ b/client/src/stores/undoRedoStore/index.ts @@ -1,4 +1,4 @@ -import { ref } from "vue"; +import { computed, ref } from "vue"; import { defineScopedStore } from "@/stores/scopedStore"; @@ -14,6 +14,7 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { const maxUndoActions = ref(100); function undo() { + flushLazyAction(); const action = undoActionStack.value.pop(); if (action !== undefined) { @@ -32,6 +33,7 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { } function applyAction(action: UndoRedoAction) { + flushLazyAction(); action.run(); clearRedoStack(); undoActionStack.value.push(action); @@ -60,6 +62,52 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { return new FactoryAction((action) => applyAction(action)); } + let lazyActionTimeout: ReturnType | undefined = undefined; + + /** action which is currently queued to run */ + const pendingLazyAction = ref(null); + + /** + * Queues an action to be applied after a delay. + * The action is applied immediately, should another action be applied, or be queued. + * You can read the `pendingLazyAction` state, or `isQueued`, to find out if the action was applied. + * + * `flushLazyAction` runs the pending lazy action immediately. + * + * `setLazyActionTimeout` can be used to extend the timeout. + * + * @param action action to queue + * @param timeout when to run the action in milliseconds. default to 1000 milliseconds + */ + function applyLazyAction(action: UndoRedoAction, timeout = 1000) { + flushLazyAction(); + clearRedoStack(); + pendingLazyAction.value = action; + lazyActionTimeout = setTimeout(() => flushLazyAction(), timeout); + } + + function clearLazyAction() { + clearTimeout(lazyActionTimeout); + pendingLazyAction.value = null; + } + + function flushLazyAction() { + clearTimeout(lazyActionTimeout); + + if (pendingLazyAction.value) { + const action = pendingLazyAction.value; + clearLazyAction(); + applyAction(action); + } + } + + function setLazyActionTimeout(timeout: number) { + clearTimeout(lazyActionTimeout); + lazyActionTimeout = setTimeout(() => flushLazyAction(), timeout); + } + + const isQueued = computed(() => (action?: UndoRedoAction | null) => pendingLazyAction.value === action); + return { undoActionStack, redoActionStack, @@ -68,6 +116,12 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { redo, applyAction, action, + applyLazyAction, + clearLazyAction, + flushLazyAction, + setLazyActionTimeout, + isQueued, + pendingLazyAction, }; }); From ecd3740b4b7e42e02c3bf3c664cb72b3e4266ba2 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:07:50 +0100 Subject: [PATCH 06/49] add undo-redo for frame movement --- .../Workflow/Editor/Actions/commentActions.ts | 85 +++++++++++++++++-- .../Workflow/Editor/Comments/FrameComment.vue | 46 ++++------ client/src/stores/undoRedoStore/README.md | 69 +++++++++++++++ client/src/stores/workflowStepStore.ts | 2 + 4 files changed, 165 insertions(+), 37 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index d57efd220e1e..264aa82a3b86 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -5,6 +5,7 @@ import type { WorkflowCommentColor, WorkflowCommentStore, } from "@/stores/workflowEditorCommentStore"; +import { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; class CommentAction extends UndoRedoAction { protected store: WorkflowCommentStore; @@ -64,7 +65,6 @@ class MutateCommentAction extends UndoRedoActio private commentId: number; private startData: WorkflowComment[K]; private endData: WorkflowComment[K]; - private ran = false; protected applyDataCallback: (commentId: number, data: WorkflowComment[K]) => void; constructor( @@ -82,17 +82,12 @@ class MutateCommentAction extends UndoRedoActio } updateData(data: WorkflowComment[K]) { - if (this.ran) { - throw new Error("data of a mutation action can not be changed once the action was run"); - } else { - this.endData = data; - this.applyDataCallback(this.commentId, this.endData); - } + this.endData = data; + this.applyDataCallback(this.commentId, this.endData); } - run() { + redo() { this.applyDataCallback(this.commentId, this.endData); - this.ran = true; } undo() { @@ -120,3 +115,75 @@ export class ChangeSizeAction extends MutateCommentAction<"size"> { super(comment, "size", size, callback); } } + +type StepWithPosition = Step & { position: NonNullable }; + +export class MoveMultipleAction extends UndoRedoAction { + private commentStore; + private stepStore; + private comments; + private steps; + + private stepStartOffsets = new Map(); + private commentStartOffsets = new Map(); + + private positionFrom; + private positionTo; + + constructor( + commentStore: WorkflowCommentStore, + stepStore: WorkflowStepStore, + comments: WorkflowComment[], + steps: StepWithPosition[], + position: { x: number; y: number } + ) { + super(); + this.commentStore = commentStore; + this.stepStore = stepStore; + this.comments = [...comments]; + this.steps = [...steps]; + + this.steps.forEach((step) => { + this.stepStartOffsets.set(step.id, [step.position.left - position.x, step.position.top - position.y]); + }); + + this.comments.forEach((comment) => { + this.commentStartOffsets.set(comment.id, [ + comment.position[0] - position.x, + comment.position[1] - position.y, + ]); + }); + + this.positionFrom = { ...position }; + this.positionTo = { ...position }; + } + + changePosition(position: { x: number; y: number }) { + this.setPosition(position); + this.positionTo = { ...position }; + } + + private setPosition(position: { x: number; y: number }) { + this.steps.forEach((step) => { + const stepPosition = { left: 0, top: 0 }; + const offset = this.stepStartOffsets.get(step.id) ?? [0, 0]; + stepPosition.left = position.x + offset[0]; + stepPosition.top = position.y + offset[1]; + this.stepStore.updateStep({ ...step, position: stepPosition }); + }); + + this.comments.forEach((comment) => { + const offset = this.commentStartOffsets.get(comment.id) ?? [0, 0]; + const commentPosition = [position.x + offset[0], position.y + offset[1]] as [number, number]; + this.commentStore.changePosition(comment.id, commentPosition); + }); + } + + undo() { + this.setPosition(this.positionFrom); + } + + redo() { + this.setPosition(this.positionTo); + } +} diff --git a/client/src/components/Workflow/Editor/Comments/FrameComment.vue b/client/src/components/Workflow/Editor/Comments/FrameComment.vue index 8e316ddf8c96..496eb28625a3 100644 --- a/client/src/components/Workflow/Editor/Comments/FrameComment.vue +++ b/client/src/components/Workflow/Editor/Comments/FrameComment.vue @@ -13,6 +13,7 @@ import { useWorkflowStores } from "@/composables/workflowStores"; import type { FrameWorkflowComment, WorkflowComment, WorkflowCommentColor } from "@/stores/workflowEditorCommentStore"; import type { Step } from "@/stores/workflowStepStore"; +import { MoveMultipleAction } from "../Actions/commentActions"; import { brighterColors, darkenedColors } from "./colors"; import { useResizable } from "./useResizable"; import { selectAllText } from "./utilities"; @@ -66,7 +67,11 @@ function getInnerText() { } function saveText() { - emit("change", { ...props.comment.data, title: getInnerText() }); + const text = getInnerText(); + + if (text !== props.comment.data.title) { + emit("change", { ...props.comment.data, title: text }); + } } const showColorSelector = ref(false); @@ -91,7 +96,7 @@ function onSetColor(color: WorkflowCommentColor) { emit("set-color", color); } -const { stateStore, stepStore, commentStore } = useWorkflowStores(); +const { stateStore, stepStore, commentStore, undoRedoStore } = useWorkflowStores(); function getStepsInBounds(bounds: AxisAlignedBoundingBox) { const steps: StepWithPosition[] = []; @@ -139,8 +144,8 @@ type StepWithPosition = Step & { position: NonNullable }; let stepsInBounds: StepWithPosition[] = []; let commentsInBounds: WorkflowComment[] = []; -const stepStartOffsets = new Map(); -const commentStartOffsets = new Map(); + +let lazyAction: MoveMultipleAction | null = null; function getAABB() { const aabb = new AxisAlignedBoundingBox(); @@ -157,39 +162,25 @@ function onDragStart() { stepsInBounds = getStepsInBounds(aabb); commentsInBounds = getCommentsInBounds(aabb); - stepsInBounds.forEach((step) => { - stepStartOffsets.set(step.id, [step.position.left - aabb.x, step.position.top - aabb.y]); - }); + commentsInBounds.push(props.comment); - commentsInBounds.forEach((comment) => { - commentStartOffsets.set(comment.id, [comment.position[0] - aabb.x, comment.position[1] - aabb.y]); - }); + lazyAction = new MoveMultipleAction(commentStore, stepStore, commentsInBounds, stepsInBounds, aabb); + undoRedoStore.applyLazyAction(lazyAction); } function onDragEnd() { saveText(); stepsInBounds = []; commentsInBounds = []; - stepStartOffsets.clear(); - commentStartOffsets.clear(); + undoRedoStore.flushLazyAction(); } function onMove(position: { x: number; y: number }) { - stepsInBounds.forEach((step) => { - const stepPosition = { left: 0, top: 0 }; - const offset = stepStartOffsets.get(step.id) ?? [0, 0]; - stepPosition.left = position.x + offset[0]; - stepPosition.top = position.y + offset[1]; - stepStore.updateStep({ ...step, position: stepPosition }); - }); - - commentsInBounds.forEach((comment) => { - const offset = commentStartOffsets.get(comment.id) ?? [0, 0]; - const commentPosition = [position.x + offset[0], position.y + offset[1]] as [number, number]; - commentStore.changePosition(comment.id, commentPosition); - }); - - emit("move", [position.x, position.y]); + if (lazyAction && undoRedoStore.isQueued(lazyAction)) { + lazyAction.changePosition(position); + } else { + onDragStart(); + } } function onDoubleClick() { @@ -273,7 +264,6 @@ onMounted(() => { class="draggable-pan" @move="onMove" @mouseup="onDragEnd" - @start="onDragStart" @pan-by="(p) => emit('pan-by', p)" />
diff --git a/client/src/stores/undoRedoStore/README.md b/client/src/stores/undoRedoStore/README.md index 79639ae8af00..b157b3077852 100644 --- a/client/src/stores/undoRedoStore/README.md +++ b/client/src/stores/undoRedoStore/README.md @@ -71,3 +71,72 @@ Classes offer the advantage that they can be defined in another file, and easily They also make it easier to store and keep track of the state required by the action. The inline factory is good for short, simple actions that need little to no state. + +## Lazy Actions + +Sometimes many similar events happen in a short time frame, and we do not want to save them all as individual actions. +One example for this may be entering text. Having every individual letter as an undo action is not practical. + +This is where lazy actions come in. They can be applied, by calling `undoRedoStore.applyLazyAction`. + +When calling this function, the actual applying of the action is delayed, and as long as it hasn't entered the undo stack, we can mutate the action. + +In order to check if an action is still waiting to be applied, we can use `undoRedoStore.isQueued`. +As long as this check returns true, it is save to mutate the action. + +Applying any action, or applying a new lazy action, will apply the currently pending action and push it to the undo stack. +Lazy actions can also be ran immediately, canceled, or have their time delay extended. + +Due to the additional complexity introduced by mutating action state, it is not recommended to use lazy actions together with the factory api. + +Here is an example of a lazy action in action. + +```ts +class ChangeCommentPositionAction extends UndoRedoAction { + private store: WorkflowCommentStore; + private commentId: number; + private startPosition: Position; + private endPosition: Position; + + constructor( + store: WorkflowCommentStore, + comment: WorkflowComment, + position: Position + ) { + super(); + this.store + this.commentId = comment.id; + this.startPosition = structuredClone(position); + this.endPosition = structuredClone(position); + this.store.changePosition(this.commentId, position); + } + + updatePosition(position: Position) { + this.endPosition = position; + this.store.changePosition(this.commentId, position); + } + + redo() { + this.store.changePosition(this.commentId, this.endPosition); + } + + undo() { + this.store.changePosition(this.commentId, this.startPosition); + } +} +``` + +In this example, we would call `updatePosition` as long as the action hasn't been applied to the undo stack. + +```ts +let lazyAction: ChangeCommentPositionAction | null = null; + +function onCommentChangePosition(position: Position) { + if (lazyAction && undoRedoStore.isQueued(lazyAction)) { + lazyAction.changePosition(position); + } else { + lazyAction = new ChangeCommentPositionAction(commentStore, comment, position); + undoRedoStore.applyLazyAction(lazyAction); + } +} +``` diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index e1ee49f479df..9bd995ea19f7 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -137,6 +137,8 @@ interface StepInputMapOver { [index: number]: { [index: string]: CollectionTypeDescriptor }; } +export type WorkflowStepStore = ReturnType; + export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (workflowId) => { const steps = ref({}); const stepMapOver = ref<{ [index: number]: CollectionTypeDescriptor }>({}); From 5f1c80a25ad9ceb700121dcf34b2a1c034df951e Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:12:52 +0100 Subject: [PATCH 07/49] reduce number of change events emitted --- .../components/Workflow/Editor/Comments/MarkdownComment.vue | 2 +- .../src/components/Workflow/Editor/Comments/TextComment.vue | 6 +++++- client/src/stores/undoRedoStore/README.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/components/Workflow/Editor/Comments/MarkdownComment.vue b/client/src/components/Workflow/Editor/Comments/MarkdownComment.vue index b052dbdc508c..ac4841b3c293 100644 --- a/client/src/components/Workflow/Editor/Comments/MarkdownComment.vue +++ b/client/src/components/Workflow/Editor/Comments/MarkdownComment.vue @@ -107,7 +107,7 @@ function onSetColor(color: WorkflowCommentColor) { function onTextChange() { const element = markdownTextarea.value; - if (element) { + if (element && element.value !== props.comment.data.text) { emit("change", { text: element.value }); } } diff --git a/client/src/components/Workflow/Editor/Comments/TextComment.vue b/client/src/components/Workflow/Editor/Comments/TextComment.vue index d3ede0bcfe3c..8139921c69c4 100644 --- a/client/src/components/Workflow/Editor/Comments/TextComment.vue +++ b/client/src/components/Workflow/Editor/Comments/TextComment.vue @@ -65,7 +65,11 @@ function getInnerText() { } function saveText() { - emit("change", { ...props.comment.data, text: getInnerText() }); + const text = getInnerText(); + + if (text !== props.comment.data.text) { + emit("change", { ...props.comment.data, text }); + } } function toggleBold() { diff --git a/client/src/stores/undoRedoStore/README.md b/client/src/stores/undoRedoStore/README.md index b157b3077852..e82817fb0246 100644 --- a/client/src/stores/undoRedoStore/README.md +++ b/client/src/stores/undoRedoStore/README.md @@ -133,7 +133,7 @@ let lazyAction: ChangeCommentPositionAction | null = null; function onCommentChangePosition(position: Position) { if (lazyAction && undoRedoStore.isQueued(lazyAction)) { - lazyAction.changePosition(position); + lazyAction.updatePosition(position); } else { lazyAction = new ChangeCommentPositionAction(commentStore, comment, position); undoRedoStore.applyLazyAction(lazyAction); From b6d4311025d7ae3c403ac356170938fa8b413c68 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:29:11 +0100 Subject: [PATCH 08/49] add tips and tricks section to documentation --- client/src/stores/undoRedoStore/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/client/src/stores/undoRedoStore/README.md b/client/src/stores/undoRedoStore/README.md index e82817fb0246..04287025f481 100644 --- a/client/src/stores/undoRedoStore/README.md +++ b/client/src/stores/undoRedoStore/README.md @@ -140,3 +140,25 @@ function onCommentChangePosition(position: Position) { } } ``` + +## Tips and Tricks + +**Do not rely on data which may be deleted by an Action** + +For example, relying on a vue component instance, or any object instance for that matter may break, +if the object gets deleted and re-created. + +Instead treat all data as if it were serializable. + +**Operate on stores as much as you can** + +The safest thing to mutate from an Undo-Redo Action is a store, since they are singletons by nature, +and you can be fairly certain that they will be around as long as your UndoRedoStore instance will be. + +**Use shallow and deep copies to avoid state mutation** + +Accidentally mutating the state of an action once it is applies breaks the undo-redo stack. +Undoing or Redoing an action may now no longer yield the same results. + +Using shallow copies (`{ ...object }; [ ...array ];`) or deep copies (`structuredClone(object)`), +avoids accidental mutation. From 6836d7b301ff5065ad51ec3a24739eea03eaa676 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:20:27 +0100 Subject: [PATCH 09/49] add workflow undo actions --- .../Workflow/Editor/Actions/commentActions.ts | 2 +- .../Editor/Actions/workflowActions.ts | 48 ++++++++++ .../src/components/Workflow/Editor/Index.vue | 90 +++++++++++++++---- 3 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 client/src/components/Workflow/Editor/Actions/workflowActions.ts diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index 264aa82a3b86..49c9b70b3f68 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -5,7 +5,7 @@ import type { WorkflowCommentColor, WorkflowCommentStore, } from "@/stores/workflowEditorCommentStore"; -import { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; +import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; class CommentAction extends UndoRedoAction { protected store: WorkflowCommentStore; diff --git a/client/src/components/Workflow/Editor/Actions/workflowActions.ts b/client/src/components/Workflow/Editor/Actions/workflowActions.ts new file mode 100644 index 000000000000..a493c99a8906 --- /dev/null +++ b/client/src/components/Workflow/Editor/Actions/workflowActions.ts @@ -0,0 +1,48 @@ +import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; + +export class LazySetValueAction extends UndoRedoAction { + setValueHandler; + fromValue; + toValue; + + constructor(fromValue: T, toValue: T, setValueHandler: (value: T) => void) { + super(); + this.fromValue = structuredClone(fromValue); + this.toValue = structuredClone(toValue); + this.setValueHandler = setValueHandler; + this.setValueHandler(toValue); + } + + changeValue(value: T) { + this.toValue = structuredClone(value); + this.setValueHandler(this.toValue); + } + + undo() { + this.setValueHandler(this.fromValue); + } + + redo() { + this.setValueHandler(this.toValue); + } +} + +export class SetValueActionHandler { + undoRedoStore; + setValueHandler; + lazyAction: LazySetValueAction | null = null; + + constructor(undoRedoStore: UndoRedoStore, setValueHandler: (value: T) => void) { + this.undoRedoStore = undoRedoStore; + this.setValueHandler = setValueHandler; + } + + set(from: T, to: T) { + if (this.lazyAction && this.undoRedoStore.isQueued(this.lazyAction)) { + this.lazyAction.changeValue(to); + } else { + this.lazyAction = new LazySetValueAction(from, to, this.setValueHandler); + this.undoRedoStore.applyLazyAction(this.lazyAction); + } + } +} diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 284e94d96448..b344e0c88c44 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -115,18 +115,18 @@ :id="id" :tags="tags" :parameters="parameters" - :annotation-current.sync="annotation" :annotation="annotation" - :name-current.sync="name" :name="name" :version="version" :versions="versions" :license="license" :creator="creator" @onVersion="onVersion" - @onTags="onTags" + @onTags="setTags" @onLicense="onLicense" - @onCreator="onCreator" /> + @onCreator="onCreator" + @update:nameCurrent="setName" + @update:annotationCurrent="setAnnotation" /> (name.value = value)); + /** user set name. queues an undo/redo action */ + function setName(newName) { + if (name.value !== newName) { + setNameActionHandler.set(name.value, newName); + } + } + + const report = ref({}); + const setReportActionHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (report.value = structuredClone(value)) + ); + /** user set report. queues an undo/redo action */ + function setReport(newReport) { + setReportActionHandler.set(report.value, newReport); + } + + const license = ref(null); + const setLicenseHandler = new SetValueActionHandler(undoRedoStore, (value) => (license.value = value)); + /** user set license. queues an undo/redo action */ + function setLicense(newLicense) { + if (license.value !== newLicense) { + setLicenseHandler.set(license.value, newLicense); + } + } + + const creator = ref(null); + const setCreatorHandler = new SetValueActionHandler(undoRedoStore, (value) => (creator.value = value)); + /** user set creator. queues an undo/redo action */ + function setCreator(newCreator) { + setCreatorHandler.set(creator.value, newCreator); + } + + const annotation = ref(null); + const setAnnotationHandler = new SetValueActionHandler(undoRedoStore, (value) => (annotation.value = value)); + /** user set annotation. queues an undo/redo action */ + function setAnnotation(newAnnotation) { + if (annotation.value !== newAnnotation) { + setAnnotationHandler.set(annotation.value, newAnnotation); + } + } + + const tags = ref([]); + const setTagsHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (tags.value = structuredClone(value)) + ); + /** user set tags. queues an undo/redo action */ + function setTags(newTags) { + setTagsHandler.set(tags.value, newTags); + } + const { comments } = storeToRefs(commentStore); const { getStepIndex, steps } = storeToRefs(stepStore); const { activeNodeId } = storeToRefs(stateStore); @@ -303,6 +358,18 @@ export default { return { id, + name, + setName, + report, + setReport, + license, + setLicense, + creator, + setCreator, + annotation, + setAnnotation, + tags, + setTags, connectionStore, hasChanges, hasInvalidConnections, @@ -326,13 +393,7 @@ export default { markdownText: null, versions: [], parameters: null, - report: {}, labels: {}, - license: null, - creator: null, - annotation: null, - name: "Unnamed Workflow", - tags: this.workflowTags, services: null, stateMessages: [], insertedStateMessages: [], @@ -832,21 +893,16 @@ export default { } } }, - onTags(tags) { - if (this.tags != tags) { - this.tags = tags; - } - }, onLicense(license) { if (this.license != license) { this.hasChanges = true; - this.license = license; + this.setLicense(license); } }, onCreator(creator) { if (this.creator != creator) { this.hasChanges = true; - this.creator = creator; + this.setCreator(creator); } }, onActiveNode(nodeId) { From c4d63126d5e1b55ae583657741bf6f35882d8a81 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:22:28 +0100 Subject: [PATCH 10/49] make tags in attributes reactive --- client/src/components/Workflow/Editor/Attributes.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/src/components/Workflow/Editor/Attributes.vue b/client/src/components/Workflow/Editor/Attributes.vue index 062635e65679..c0d7311e06ee 100644 --- a/client/src/components/Workflow/Editor/Attributes.vue +++ b/client/src/components/Workflow/Editor/Attributes.vue @@ -47,7 +47,7 @@
Tags - +
Apply tags to make it easy to search for and find items with the same tag.
@@ -117,7 +117,6 @@ export default { return { message: null, messageVariant: null, - tagsCurrent: this.tags, versionCurrent: this.version, annotationCurrent: this.annotation, nameCurrent: this.name, @@ -186,9 +185,8 @@ export default { }, methods: { onTags(tags) { - this.tagsCurrent = tags; this.onAttributes({ tags }); - this.$emit("onTags", this.tagsCurrent); + this.$emit("onTags", tags); }, onVersion() { this.$emit("onVersion", this.versionCurrent); From d5f86b71ac44fffb9ff5c83ba8a88f71f8c2e823 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:14:01 +0100 Subject: [PATCH 11/49] add basic step actions --- .../Workflow/Editor/Actions/stepActions.ts | 83 +++++++++++++++++++ .../src/components/Workflow/Editor/Index.vue | 28 ++----- client/src/stores/undoRedoStore/index.ts | 7 ++ client/src/stores/workflowStepStore.ts | 11 +++ 4 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 client/src/components/Workflow/Editor/Actions/stepActions.ts diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts new file mode 100644 index 000000000000..a0381157732c --- /dev/null +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -0,0 +1,83 @@ +import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; +import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; + +class LazyMutateStepAction extends UndoRedoAction { + key: K; + fromValue: Step[K]; + toValue: Step[K]; + stepId; + stepStore; + + constructor(stepStore: WorkflowStepStore, stepId: number, key: K, fromValue: Step[K], toValue: Step[K]) { + super(); + this.stepStore = stepStore; + this.stepId = stepId; + this.key = key; + this.fromValue = fromValue; + this.toValue = toValue; + + this.stepStore.updateStepValue(this.stepId, this.key, this.toValue); + } + + changeValue(value: Step[K]) { + this.toValue = value; + this.stepStore.updateStepValue(this.stepId, this.key, this.toValue); + } + + undo() { + this.stepStore.updateStepValue(this.stepId, this.key, this.fromValue); + } + + redo() { + this.stepStore.updateStepValue(this.stepId, this.key, this.toValue); + } +} + +export function useStepActions(stepStore: WorkflowStepStore, undoRedoStore: UndoRedoStore) { + /** + * If the pending action is a `LazyMutateStepAction` and matches the step id and field key, returns it. + * Otherwise returns `null` + */ + function actionForIdAndKey(id: number, key: keyof Step) { + const pendingAction = undoRedoStore.pendingLazyAction; + + if (pendingAction instanceof LazyMutateStepAction && pendingAction.stepId === id && pendingAction.key === key) { + return pendingAction; + } else { + return null; + } + } + + /** + * Mutates a queued lazy action, if a matching one exists, + * otherwise creates a new lazy action ans queues it. + */ + function changeValueOrCreateAction(step: Step, key: K, value: Step[K]) { + const actionForKey = actionForIdAndKey(step.id, key); + + if (actionForKey) { + actionForKey.changeValue(value); + } else { + const action = new LazyMutateStepAction(stepStore, step.id, key, step[key], value); + undoRedoStore.applyLazyAction(action); + } + } + + function setPosition(step: Step, position: NonNullable) { + changeValueOrCreateAction(step, "position", position); + } + + function setAnnotation(step: Step, annotation: Step["annotation"]) { + changeValueOrCreateAction(step, "annotation", annotation); + } + + function setLabel(step: Step, label: Step["label"]) { + changeValueOrCreateAction(step, "label", label); + } + + return { + setPosition, + setAnnotation, + setLabel, + }; +} diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index b344e0c88c44..8eec2b4bf5a8 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -186,6 +186,7 @@ import { LastQueue } from "@/utils/lastQueue"; import { errorMessageAsString } from "@/utils/simple-error"; import { Services } from "../services"; +import { useStepActions } from "./Actions/stepActions"; import { SetValueActionHandler } from "./Actions/workflowActions"; import { defaultPosition } from "./composables/useDefaultStepPosition"; import { fromSimple } from "./modules/model"; @@ -349,6 +350,7 @@ export default { stepStore.$reset(); stateStore.$reset(); commentStore.$reset(); + undoRedoStore.$reset(); } onUnmounted(() => { @@ -356,6 +358,8 @@ export default { emit("update:confirmation", false); }); + const stepActions = useStepActions(stepStore, undoRedoStore); + return { id, name, @@ -385,6 +389,7 @@ export default { stateStore, resetStores, initialLoading, + stepActions, }; }, data() { @@ -472,8 +477,7 @@ export default { this.stepStore.updateStep(step); }, onUpdateStepPosition(stepId, position) { - const step = { ...this.steps[stepId], position }; - this.onUpdateStep(step); + this.stepActions.setPosition(this.steps[stepId], position); }, onConnect(connection) { this.connectionStore.addConnection(connection); @@ -656,8 +660,7 @@ export default { this.showInPanel = "attributes"; }, onAnnotation(nodeId, newAnnotation) { - const step = { ...this.steps[nodeId], annotation: newAnnotation }; - this.onUpdateStep(step); + this.stepActions.setAnnotation(this.steps[nodeId], newAnnotation); }, async routeToWorkflow(id) { // map scoped stores to existing stores, before updating the id @@ -724,22 +727,7 @@ export default { this.onReportUpdate(newMarkdown); }, onLabel(nodeId, newLabel) { - const step = { ...this.steps[nodeId], label: newLabel }; - const oldLabel = this.steps[nodeId].label; - this.onUpdateStep(step); - const stepType = this.steps[nodeId].type; - const isInput = ["data_input", "data_collection_input", "parameter_input"].indexOf(stepType) >= 0; - const labelType = isInput ? "input" : "step"; - const labelTypeTitle = isInput ? "Input" : "Step"; - const newMarkdown = replaceLabel(this.markdownText, labelType, oldLabel, newLabel); - if (newMarkdown !== this.markdownText) { - this.debouncedToast(`${labelTypeTitle} label updated in workflow report.`, 1500); - } - this.onReportUpdate(newMarkdown); - }, - debouncedToast(message, delay) { - clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout(() => Toast.success(message), delay); + this.stepActions.setLabel(this.steps[nodeId], newLabel); }, onScrollTo(stepId) { this.scrollToId = stepId; diff --git a/client/src/stores/undoRedoStore/index.ts b/client/src/stores/undoRedoStore/index.ts index 173de34744a8..9e8483b4f926 100644 --- a/client/src/stores/undoRedoStore/index.ts +++ b/client/src/stores/undoRedoStore/index.ts @@ -13,6 +13,12 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { const redoActionStack = ref([]); const maxUndoActions = ref(100); + function $reset() { + undoActionStack.value.forEach((action) => action.destroy()); + undoActionStack.value = []; + clearRedoStack(); + } + function undo() { flushLazyAction(); const action = undoActionStack.value.pop(); @@ -122,6 +128,7 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { setLazyActionTimeout, isQueued, pendingLazyAction, + $reset, }; }); diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index 9bd995ea19f7..15cf99b7afe4 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -248,6 +248,16 @@ export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (work stepExtraInputs.value[step.id] = findStepExtraInputs(step); } + function updateStepValue(stepId: number, key: K, value: Step[K]) { + const step = steps.value[stepId]; + assertDefined(step); + + const partialStep: Partial = {}; + partialStep[key] = value; + + updateStep({ ...step, ...partialStep }); + } + function changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) { set(stepMapOver.value, stepId, mapOver); } @@ -361,6 +371,7 @@ export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (work addStep, insertNewStep, updateStep, + updateStepValue, changeStepMapOver, resetStepInputMapOver, changeStepInputMapOver, From 0d41f6d7a27d28e37dd5ec43e55d6e4b8513d4aa Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:49:01 +0100 Subject: [PATCH 12/49] show attributes when undoing/redoing them --- .../Editor/Actions/workflowActions.ts | 13 +++- .../src/components/Workflow/Editor/Index.vue | 71 +++++++++++++------ 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/workflowActions.ts b/client/src/components/Workflow/Editor/Actions/workflowActions.ts index a493c99a8906..c00988b68cb8 100644 --- a/client/src/components/Workflow/Editor/Actions/workflowActions.ts +++ b/client/src/components/Workflow/Editor/Actions/workflowActions.ts @@ -2,14 +2,17 @@ import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; export class LazySetValueAction extends UndoRedoAction { setValueHandler; + showAttributesCallback; fromValue; toValue; - constructor(fromValue: T, toValue: T, setValueHandler: (value: T) => void) { + constructor(fromValue: T, toValue: T, setValueHandler: (value: T) => void, showCanvasCallback: () => void) { super(); this.fromValue = structuredClone(fromValue); this.toValue = structuredClone(toValue); this.setValueHandler = setValueHandler; + this.showAttributesCallback = showCanvasCallback; + this.setValueHandler(toValue); } @@ -19,10 +22,12 @@ export class LazySetValueAction extends UndoRedoAction { } undo() { + this.showAttributesCallback(); this.setValueHandler(this.fromValue); } redo() { + this.showAttributesCallback(); this.setValueHandler(this.toValue); } } @@ -30,18 +35,20 @@ export class LazySetValueAction extends UndoRedoAction { export class SetValueActionHandler { undoRedoStore; setValueHandler; + showAttributesCallback; lazyAction: LazySetValueAction | null = null; - constructor(undoRedoStore: UndoRedoStore, setValueHandler: (value: T) => void) { + constructor(undoRedoStore: UndoRedoStore, setValueHandler: (value: T) => void, showCanvasCallback: () => void) { this.undoRedoStore = undoRedoStore; this.setValueHandler = setValueHandler; + this.showAttributesCallback = showCanvasCallback; } set(from: T, to: T) { if (this.lazyAction && this.undoRedoStore.isQueued(this.lazyAction)) { this.lazyAction.changeValue(to); } else { - this.lazyAction = new LazySetValueAction(from, to, this.setValueHandler); + this.lazyAction = new LazySetValueAction(from, to, this.setValueHandler, this.showAttributesCallback); this.undoRedoStore.applyLazyAction(this.lazyAction); } } diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 8eec2b4bf5a8..1afb176c767d 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -111,7 +111,7 @@ @onUpdateStep="onUpdateStep" @onSetData="onSetData" /> (name.value = value)); + const setNameActionHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (name.value = value), + showAttributes + ); /** user set name. queues an undo/redo action */ function setName(newName) { if (name.value !== newName) { @@ -274,9 +294,12 @@ export default { } const report = ref({}); + + // TODO: move report undo redo to report editor const setReportActionHandler = new SetValueActionHandler( undoRedoStore, - (value) => (report.value = structuredClone(value)) + (value) => (report.value = structuredClone(value)), + showAttributes ); /** user set report. queues an undo/redo action */ function setReport(newReport) { @@ -284,7 +307,11 @@ export default { } const license = ref(null); - const setLicenseHandler = new SetValueActionHandler(undoRedoStore, (value) => (license.value = value)); + const setLicenseHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (license.value = value), + showAttributes + ); /** user set license. queues an undo/redo action */ function setLicense(newLicense) { if (license.value !== newLicense) { @@ -293,14 +320,22 @@ export default { } const creator = ref(null); - const setCreatorHandler = new SetValueActionHandler(undoRedoStore, (value) => (creator.value = value)); + const setCreatorHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (creator.value = value), + showAttributes + ); /** user set creator. queues an undo/redo action */ function setCreator(newCreator) { setCreatorHandler.set(creator.value, newCreator); } const annotation = ref(null); - const setAnnotationHandler = new SetValueActionHandler(undoRedoStore, (value) => (annotation.value = value)); + const setAnnotationHandler = new SetValueActionHandler( + undoRedoStore, + (value) => (annotation.value = value), + showAttributes + ); /** user set annotation. queues an undo/redo action */ function setAnnotation(newAnnotation) { if (annotation.value !== newAnnotation) { @@ -311,7 +346,8 @@ export default { const tags = ref([]); const setTagsHandler = new SetValueActionHandler( undoRedoStore, - (value) => (tags.value = structuredClone(value)) + (value) => (tags.value = structuredClone(value)), + showAttributes ); /** user set tags. queues an undo/redo action */ function setTags(newTags) { @@ -363,6 +399,11 @@ export default { return { id, name, + isCanvas, + parameters, + ensureParametersSet, + showInPanel, + showAttributes, setName, report, setReport, @@ -394,10 +435,9 @@ export default { }, data() { return { - isCanvas: true, + markdownConfig: null, markdownText: null, versions: [], - parameters: null, labels: {}, services: null, stateMessages: [], @@ -409,7 +449,6 @@ export default { messageBody: null, messageIsError: false, version: this.initialVersion, - showInPanel: "attributes", saveAsName: null, saveAsAnnotation: null, showSaveAsModal: false, @@ -421,7 +460,7 @@ export default { }; }, computed: { - showAttributes() { + attributesVisible() { return this.showInPanel == "attributes"; }, showLint() { @@ -650,11 +689,6 @@ export default { }); }); }, - onAttributes() { - this._ensureParametersSet(); - this.stateStore.activeNodeId = null; - this.showInPanel = "attributes"; - }, onWorkflowTextEditor() { this.stateStore.activeNodeId = null; this.showInPanel = "attributes"; @@ -740,7 +774,7 @@ export default { this.highlightId = null; }, onLint() { - this._ensureParametersSet(); + this.ensureParametersSet(); this.stateStore.activeNodeId = null; this.showInPanel = "lint"; }, @@ -813,9 +847,6 @@ export default { this._loadCurrent(this.id, version); } }, - _ensureParametersSet() { - this.parameters = getUntypedWorkflowParameters(this.steps); - }, _insertStep(contentId, name, type) { if (!this.isCanvas) { this.isCanvas = true; From 9c63cd538d0b1001ec592f220dc546903d32ee01 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:21:39 +0100 Subject: [PATCH 13/49] switch to node on node attribute edit --- .../Workflow/Editor/Actions/stepActions.ts | 14 +++++++++++++- client/src/components/Workflow/Editor/Index.vue | 8 ++++---- client/src/stores/workflowEditorStateStore.ts | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index a0381157732c..d9a7fc8a1b7f 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -1,4 +1,5 @@ import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; +import { WorkflowStateStore } from "@/stores/workflowEditorStateStore"; import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; class LazyMutateStepAction extends UndoRedoAction { @@ -7,6 +8,7 @@ class LazyMutateStepAction extends UndoRedoAction { toValue: Step[K]; stepId; stepStore; + onUndoRedo?: () => void; constructor(stepStore: WorkflowStepStore, stepId: number, key: K, fromValue: Step[K], toValue: Step[K]) { super(); @@ -26,14 +28,20 @@ class LazyMutateStepAction extends UndoRedoAction { undo() { this.stepStore.updateStepValue(this.stepId, this.key, this.fromValue); + this.onUndoRedo?.(); } redo() { this.stepStore.updateStepValue(this.stepId, this.key, this.toValue); + this.onUndoRedo?.(); } } -export function useStepActions(stepStore: WorkflowStepStore, undoRedoStore: UndoRedoStore) { +export function useStepActions( + stepStore: WorkflowStepStore, + undoRedoStore: UndoRedoStore, + stateStore: WorkflowStateStore +) { /** * If the pending action is a `LazyMutateStepAction` and matches the step id and field key, returns it. * Otherwise returns `null` @@ -60,6 +68,10 @@ export function useStepActions(stepStore: WorkflowStepStore, undoRedoStore: Undo } else { const action = new LazyMutateStepAction(stepStore, step.id, key, step[key], value); undoRedoStore.applyLazyAction(action); + + action.onUndoRedo = () => { + stateStore.activeNodeId = step.id; + }; } } diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 1afb176c767d..daf377616bf4 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -81,7 +81,7 @@ @onReport="onReport" @onLayout="onLayout" @onEdit="onEdit" - @onAttributes="onAttributes" + @onAttributes="showAttributes" @onLint="onLint" @onUpgrade="onUpgrade" />
@@ -135,7 +135,7 @@ :license="license" :steps="steps" :datatypes-mapper="datatypesMapper" - @onAttributes="onAttributes" + @onAttributes="showAttributes" @onHighlight="onHighlight" @onUnhighlight="onUnhighlight" @onRefactor="onAttemptRefactor" @@ -394,7 +394,7 @@ export default { emit("update:confirmation", false); }); - const stepActions = useStepActions(stepStore, undoRedoStore); + const stepActions = useStepActions(stepStore, undoRedoStore, stateStore); return { id, @@ -731,7 +731,7 @@ export default { nameValidate() { if (!this.name) { Toast.error("Please provide a name for your workflow."); - this.onAttributes(); + this.showAttributes(); return false; } return true; diff --git a/client/src/stores/workflowEditorStateStore.ts b/client/src/stores/workflowEditorStateStore.ts index 731153b1ce1b..1f8f3f6743f8 100644 --- a/client/src/stores/workflowEditorStateStore.ts +++ b/client/src/stores/workflowEditorStateStore.ts @@ -28,6 +28,8 @@ type OutputTerminalPositions = { [index: number]: { [index: string]: OutputTermi type StepPosition = { [index: number]: UnwrapRef }; type StepLoadingState = { [index: number]: { loading?: boolean; error?: string } }; +export type WorkflowStateStore = ReturnType; + export const useWorkflowStateStore = defineScopedStore("workflowStateStore", () => { const inputTerminals = ref({}); const outputTerminals = ref({}); From 516f204cf808ff48c3809cc866e00ab9571e7f24 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:43:24 +0100 Subject: [PATCH 14/49] add set data action --- .../Workflow/Editor/Actions/stepActions.ts | 45 +++++++++++++++++++ .../src/components/Workflow/Editor/Index.vue | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index d9a7fc8a1b7f..145fbffa187c 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -1,6 +1,7 @@ import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; import { WorkflowStateStore } from "@/stores/workflowEditorStateStore"; import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; +import { assertDefined } from "@/utils/assertions"; class LazyMutateStepAction extends UndoRedoAction { key: K; @@ -37,6 +38,44 @@ class LazyMutateStepAction extends UndoRedoAction { } } +export class SetDataAction extends UndoRedoAction { + stepStore; + stateStore; + stepId; + fromPartial: Partial = {}; + toPartial: Partial = {}; + + constructor(stepStore: WorkflowStepStore, stateStore: WorkflowStateStore, from: Step, to: Step) { + super(); + this.stepStore = stepStore; + this.stateStore = stateStore; + this.stepId = from.id; + + Object.entries(from).forEach(([key, value]) => { + const otherValue = to[key as keyof Step] as any; + + if (JSON.stringify(value) !== JSON.stringify(otherValue)) { + this.fromPartial[key as keyof Step] = structuredClone(value); + this.toPartial[key as keyof Step] = structuredClone(otherValue); + } + }); + } + + run() { + const step = this.stepStore.getStep(this.stepId); + assertDefined(step); + this.stateStore.activeNodeId = this.stepId; + this.stepStore.updateStep({ ...step, ...this.toPartial }); + } + + undo() { + const step = this.stepStore.getStep(this.stepId); + assertDefined(step); + this.stateStore.activeNodeId = this.stepId; + this.stepStore.updateStep({ ...step, ...this.fromPartial }); + } +} + export function useStepActions( stepStore: WorkflowStepStore, undoRedoStore: UndoRedoStore, @@ -87,9 +126,15 @@ export function useStepActions( changeValueOrCreateAction(step, "label", label); } + function setData(from: Step, to: Step) { + const action = new SetDataAction(stepStore, stateStore, from, to); + undoRedoStore.applyAction(action); + } + return { setPosition, setAnnotation, setLabel, + setData, }; } diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index daf377616bf4..093879c62e0a 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -750,7 +750,7 @@ export default { tool_version: data.tool_version, errors: data.errors, }; - this.onUpdateStep(step); + this.stepActions.setData(this.steps[stepId], step); }); }, onOutputLabel(oldValue, newValue) { From b97b9e79c896e2e5c2e15a145c6f8bb491fbf685 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:11:04 +0100 Subject: [PATCH 15/49] remove unused emit --- client/src/components/Workflow/Editor/Forms/FormTool.vue | 2 +- client/src/components/Workflow/Editor/Index.vue | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.vue b/client/src/components/Workflow/Editor/Forms/FormTool.vue index 919cbad7a7b0..dce544f66a11 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.vue +++ b/client/src/components/Workflow/Editor/Forms/FormTool.vue @@ -89,7 +89,7 @@ export default { required: true, }, }, - emits: ["onSetData", "onUpdateStep", "onChangePostJobActions", "onAnnotation", "onLabel", "onOutputLabel"], + emits: ["onSetData", "onChangePostJobActions", "onAnnotation", "onLabel"], setup(props, { emit }) { const { stepId, annotation, label, stepInputs, stepOutputs, configForm, postJobActions } = useStepProps( toRef(props, "step") diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 093879c62e0a..96ddf806e838 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -96,8 +96,6 @@ @onChangePostJobActions="onChangePostJobActions" @onAnnotation="onAnnotation" @onLabel="onLabel" - @onOutputLabel="onOutputLabel" - @onUpdateStep="onUpdateStep" @onSetData="onSetData" /> Date: Tue, 12 Mar 2024 11:43:24 +0100 Subject: [PATCH 16/49] fix form undo reactivity --- .../Workflow/Editor/Actions/stepActions.ts | 11 ++++++ .../Workflow/Editor/Forms/FormDefault.vue | 34 +++++++++++++------ .../Workflow/Editor/Forms/FormTool.vue | 16 +++++++-- client/src/stores/refreshFromStore.ts | 26 ++++++++++++++ 4 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 client/src/stores/refreshFromStore.ts diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index 145fbffa187c..b5933779e55f 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -1,3 +1,4 @@ +import { useRefreshFromStore } from "@/stores/refreshFromStore"; import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; import { WorkflowStateStore } from "@/stores/workflowEditorStateStore"; import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; @@ -44,6 +45,7 @@ export class SetDataAction extends UndoRedoAction { stepId; fromPartial: Partial = {}; toPartial: Partial = {}; + refreshForm: () => void; constructor(stepStore: WorkflowStepStore, stateStore: WorkflowStateStore, from: Step, to: Step) { super(); @@ -59,6 +61,9 @@ export class SetDataAction extends UndoRedoAction { this.toPartial[key as keyof Step] = structuredClone(otherValue); } }); + + const { refresh } = useRefreshFromStore(); + this.refreshForm = refresh; } run() { @@ -73,6 +78,12 @@ export class SetDataAction extends UndoRedoAction { assertDefined(step); this.stateStore.activeNodeId = this.stepId; this.stepStore.updateStep({ ...step, ...this.fromPartial }); + this.refreshForm(); + } + + redo() { + this.run(); + this.refreshForm(); } } diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.vue b/client/src/components/Workflow/Editor/Forms/FormDefault.vue index 72e786d3b6d3..e846b86b1568 100644 --- a/client/src/components/Workflow/Editor/Forms/FormDefault.vue +++ b/client/src/components/Workflow/Editor/Forms/FormDefault.vue @@ -43,6 +43,7 @@
@@ -59,11 +60,13 @@ diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.vue b/client/src/components/Workflow/Editor/Forms/FormTool.vue index dce544f66a11..5dcb91afff92 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.vue +++ b/client/src/components/Workflow/Editor/Forms/FormTool.vue @@ -30,6 +30,7 @@ Tool Parameters diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.vue b/client/src/components/Workflow/Editor/Forms/FormDefault.vue index e846b86b1568..3fcf1a669c44 100644 --- a/client/src/components/Workflow/Editor/Forms/FormDefault.vue +++ b/client/src/components/Workflow/Editor/Forms/FormDefault.vue @@ -39,7 +39,10 @@ :area="true" help="Add an annotation or notes to this step. Annotations are available when a workflow is viewed." @input="onAnnotation" /> - + - +
Tool Parameters + @onSetData="onSetData" + @onUpdateStep="updateStep" /> + @onSetData="onSetData" + @onUpdateStep="updateStep" /> { - this.onUpdateStep({ + this.stepStore.updateStep({ ...this.steps[step.id], config_form: response.config_form, content_id: response.content_id, @@ -596,8 +601,7 @@ export default { this.stepActions.setData(step, updatedStep); }, onRemove(nodeId) { - this.stepStore.removeStep(nodeId); - this.showInPanel = "attributes"; + this.stepActions.removeStep(this.steps[nodeId], this.showAttributes); }, onEditSubworkflow(contentId) { const editUrl = `/workflows/edit?workflow_id=${contentId}`; @@ -682,7 +686,7 @@ export default { onLayout() { return import(/* webpackChunkName: "workflowLayout" */ "./modules/layout.ts").then((layout) => { layout.autoLayout(this.id, this.steps).then((newSteps) => { - newSteps.map((step) => this.onUpdateStep(step)); + newSteps.map((step) => this.stepStore.updateStep(step)); }); }); }, From 20d2644c5d7a597c51f6fd3c9b53a43f3a98287c Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:13:19 +0100 Subject: [PATCH 20/49] fix reactivity in post job actions form --- client/src/components/Workflow/Editor/Forms/FormTool.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.vue b/client/src/components/Workflow/Editor/Forms/FormTool.vue index 5d17c1e6c763..279027223000 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.vue +++ b/client/src/components/Workflow/Editor/Forms/FormTool.vue @@ -42,6 +42,7 @@ Additional Options Date: Tue, 12 Mar 2024 15:18:01 +0100 Subject: [PATCH 21/49] replace setData with updateStep --- client/src/components/Workflow/Editor/Index.vue | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 4a97618461be..7478e0885378 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -596,9 +596,8 @@ export default { this.hasChanges = true; }, onChangePostJobActions(nodeId, postJobActions) { - const step = this.steps[nodeId]; - const updatedStep = { ...step, post_job_actions: postJobActions }; - this.stepActions.setData(step, updatedStep); + const partialStep = { post_job_actions: postJobActions }; + this.stepActions.updateStep(nodeId, partialStep); }, onRemove(nodeId) { this.stepActions.removeStep(this.steps[nodeId], this.showAttributes); @@ -741,8 +740,7 @@ export default { this.lastQueue .enqueue(() => getModule(newData, stepId, this.stateStore.setLoadingState)) .then((data) => { - const step = { - ...this.steps[stepId], + const partialStep = { content_id: data.content_id, inputs: data.inputs, outputs: data.outputs, @@ -751,7 +749,7 @@ export default { tool_version: data.tool_version, errors: data.errors, }; - this.stepActions.setData(this.steps[stepId], step); + this.stepActions.updateStep(stepId, partialStep); }); }, onOutputLabel(oldValue, newValue) { From 3c5ee1e5a0f2c37e0200c78ad64d22e2a50b4979 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:41:43 +0100 Subject: [PATCH 22/49] reduce emitted events --- .../Workflow/Editor/Forms/FormSection.vue | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/components/Workflow/Editor/Forms/FormSection.vue b/client/src/components/Workflow/Editor/Forms/FormSection.vue index d7b96dfc9591..8067c7f361af 100644 --- a/client/src/components/Workflow/Editor/Forms/FormSection.vue +++ b/client/src/components/Workflow/Editor/Forms/FormSection.vue @@ -92,8 +92,11 @@ export default { return Boolean(this.formData[this.deleteActionKey]); }, }, - watch: { - formData() { + created() { + this.setFormData(); + }, + methods: { + postPostJobActions() { // The formData shape is kind of unfortunate, but it is what we have now. // This should be a properly nested object whose values should be retrieved and set via a store const postJobActions = {}; @@ -119,11 +122,6 @@ export default { }); this.$emit("onChange", postJobActions); }, - }, - created() { - this.setFormData(); - }, - methods: { setFormData() { const pjas = {}; Object.values(this.postJobActions).forEach((pja) => { @@ -168,6 +166,7 @@ export default { this.setEmailAction(this.formData); if (changed) { this.formData = Object.assign({}, this.formData); + this.postPostJobActions(); } }, onDatatype(pjaKey, outputName, newDatatype) { From ace5fa76ba97b4a02cab8caa6a88d0116c179dfe Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:45:58 +0100 Subject: [PATCH 23/49] fix form reactivity for update action --- .../Workflow/Editor/Actions/stepActions.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index 094aa876247a..828b7e2af4d7 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -180,11 +180,13 @@ export class RemoveStepAction extends UndoRedoAction { run() { this.stepStore.removeStep(this.step.id); this.showAttributesCallback(); + this.stateStore.hasChanges = true; } undo() { this.stepStore.addStep(structuredClone(this.step)); this.stateStore.activeNodeId = this.step.id; + this.stateStore.hasChanges = true; } } @@ -269,7 +271,15 @@ export function useStepActions( }); const action = new UpdateStepAction(stepStore, stateStore, id, fromPartial, toPartial); - undoRedoStore.applyAction(action); + + if (!action.isEmpty()) { + action.onUndoRedo = () => { + stateStore.activeNodeId = id; + stateStore.hasChanges = true; + refresh(); + }; + undoRedoStore.applyAction(action); + } } return { From 679c6b62586b69083a08e56e9f18cf1048c0fc95 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:59:19 +0100 Subject: [PATCH 24/49] add copy step undo action --- .../Workflow/Editor/Actions/stepActions.ts | 30 +++++++++++++++++++ .../src/components/Workflow/Editor/Index.vue | 6 ++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index 828b7e2af4d7..afe3fb2d857c 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -190,6 +190,30 @@ export class RemoveStepAction extends UndoRedoAction { } } +export class CopyStepAction extends UndoRedoAction { + stepStore; + stateStore; + step; + onUndoRedo?: () => void; + + constructor(stepStore: WorkflowStepStore, stateStore: WorkflowStateStore, step: Step) { + super(); + this.stepStore = stepStore; + this.stateStore = stateStore; + this.step = structuredClone(step); + } + + run() { + this.step = this.stepStore.addStep(this.step); + this.stateStore.activeNodeId = this.step.id; + this.stateStore.hasChanges = true; + } + + undo() { + this.stepStore.removeStep(this.step.id); + } +} + export function useStepActions( stepStore: WorkflowStepStore, undoRedoStore: UndoRedoStore, @@ -282,6 +306,11 @@ export function useStepActions( } } + function copyStep(step: Step) { + const action = new CopyStepAction(stepStore, stateStore, step); + undoRedoStore.applyAction(action); + } + return { setPosition, setAnnotation, @@ -289,5 +318,6 @@ export function useStepActions( setData, removeStep, updateStep, + copyStep, }; } diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 7478e0885378..012f598853a1 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -608,15 +608,13 @@ export default { }, async onClone(stepId) { const sourceStep = this.steps[parseInt(stepId)]; - const stepCopy = JSON.parse(JSON.stringify(sourceStep)); - const { id } = this.stepStore.addStep({ - ...stepCopy, + this.stepActions.copyStep({ + ...sourceStep, id: null, uuid: null, label: null, position: defaultPosition(this.graphOffset, this.transform), }); - this.stateStore.activeNodeId = id; }, onInsertTool(tool_id, tool_name) { this._insertStep(tool_id, tool_name, "tool"); From 5b81b687210dff7647b572df7a318f90af8a85b9 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:20:03 +0100 Subject: [PATCH 25/49] remove unused events --- client/src/components/Workflow/Editor/Index.vue | 6 ------ client/src/components/Workflow/Editor/Node.vue | 1 - 2 files changed, 7 deletions(-) diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 012f598853a1..3d348dbe66c3 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -55,13 +55,10 @@ @scrollTo="scrollToId = null" @transform="(value) => (transform = value)" @graph-offset="(value) => (graphOffset = value)" - @onUpdate="onUpdate" @onClone="onClone" @onCreate="onInsertTool" @onChange="onChange" - @onConnect="onConnect" @onRemove="onRemove" - @onUpdateStep="onUpdateStep" @onUpdateStepPosition="onUpdateStepPosition">
@@ -520,9 +517,6 @@ export default { onUpdateStepPosition(stepId, position) { this.stepActions.setPosition(this.steps[stepId], position); }, - onConnect(connection) { - this.connectionStore.addConnection(connection); - }, onAttemptRefactor(actions) { if (this.hasChanges) { const r = window.confirm( diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue index e152292f2db3..3b29ed433f27 100644 --- a/client/src/components/Workflow/Editor/Node.vue +++ b/client/src/components/Workflow/Editor/Node.vue @@ -172,7 +172,6 @@ const emit = defineEmits([ "onActivate", "onChange", "onCreate", - "onUpdate", "onClone", "onUpdateStepPosition", "pan-by", From 05325cc408f9816bf39d3ded50292c16ae6fa991 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:35:59 +0100 Subject: [PATCH 26/49] add actions for output visible and active --- .../components/Workflow/Editor/NodeOutput.vue | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/client/src/components/Workflow/Editor/NodeOutput.vue b/client/src/components/Workflow/Editor/NodeOutput.vue index 3d21e05105ed..af3c571edba6 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.vue +++ b/client/src/components/Workflow/Editor/NodeOutput.vue @@ -22,8 +22,9 @@ import { type PostJobActions, type Step, } from "@/stores/workflowStepStore"; -import { assertDefined, ensureDefined } from "@/utils/assertions"; +import { assertDefined } from "@/utils/assertions"; +import { UpdateStepAction } from "./Actions/stepActions"; import { useRelativePosition } from "./composables/relativePosition"; import { useTerminal } from "./composables/useTerminal"; import { type CollectionTypeDescriptor, NULL_COLLECTION_TYPE_DESCRIPTION } from "./modules/collectionTypeDescription"; @@ -53,7 +54,7 @@ const props = defineProps<{ }>(); const emit = defineEmits(["pan-by", "stopDragging", "onDragConnector"]); -const { stateStore, stepStore } = useWorkflowStores(); +const { stateStore, stepStore, undoRedoStore } = useWorkflowStores(); const { rootOffset, output, stepId, datatypesMapper } = toRefs(props); const terminalComponent: Ref | null> = ref(null); @@ -155,7 +156,15 @@ function onToggleActive() { } else { stepWorkflowOutputs.push({ output_name: output.value.name, label: output.value.name }); } - stepStore.updateStep({ ...step, workflow_outputs: stepWorkflowOutputs }); + + const action = new UpdateStepAction( + stepStore, + stateStore, + step.id, + { workflow_outputs: step.workflow_outputs }, + { workflow_outputs: stepWorkflowOutputs } + ); + undoRedoStore.applyAction(action); } function onToggleVisible() { @@ -164,25 +173,36 @@ function onToggleVisible() { } const actionKey = `HideDatasetAction${props.output.name}`; - const step = { ...ensureDefined(stepStore.getStep(stepId.value)) }; + const step = stepStore.getStep(stepId.value); + assertDefined(step); + + const oldPostJobActions = structuredClone(step.post_job_actions) ?? {}; + let newPostJobActions; + if (isVisible.value) { - step.post_job_actions = { - ...step.post_job_actions, - [actionKey]: { - action_type: "HideDatasetAction", - output_name: props.output.name, - action_arguments: {}, - }, + newPostJobActions = structuredClone(step.post_job_actions) ?? {}; + newPostJobActions[actionKey] = { + action_type: "HideDatasetAction", + output_name: props.output.name, + action_arguments: {}, }; } else { if (step.post_job_actions) { - const { [actionKey]: _unused, ...newPostJobActions } = step.post_job_actions; - step.post_job_actions = newPostJobActions; + const { [actionKey]: _unused, ...remainingPostJobActions } = step.post_job_actions; + newPostJobActions = structuredClone(remainingPostJobActions); } else { - step.post_job_actions = {}; + newPostJobActions = {}; } } - stepStore.updateStep(step); + + const action = new UpdateStepAction( + stepStore, + stateStore, + step.id, + { post_job_actions: oldPostJobActions }, + { post_job_actions: newPostJobActions } + ); + undoRedoStore.applyAction(action); } function onPanBy(panBy: XYPosition) { From 09e945db5d48dfcff3a766fade3ef16f433716c5 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:11:16 +0100 Subject: [PATCH 27/49] refactor terminals to have access to all stores --- .../Workflow/Editor/ConnectionMenu.vue | 5 +- .../src/components/Workflow/Editor/Lint.vue | 15 +- .../components/Workflow/Editor/NodeInput.vue | 6 +- .../Workflow/Editor/NodeOutput.test.ts | 23 ++- .../components/Workflow/Editor/NodeOutput.vue | 2 +- .../Editor/composables/useTerminal.ts | 14 +- .../Workflow/Editor/modules/linting.ts | 14 +- .../Workflow/Editor/modules/terminals.test.ts | 83 ++++----- .../Workflow/Editor/modules/terminals.ts | 164 +++++++----------- 9 files changed, 136 insertions(+), 190 deletions(-) diff --git a/client/src/components/Workflow/Editor/ConnectionMenu.vue b/client/src/components/Workflow/Editor/ConnectionMenu.vue index a8bf0b00e832..e97cc24a4ad3 100644 --- a/client/src/components/Workflow/Editor/ConnectionMenu.vue +++ b/client/src/components/Workflow/Editor/ConnectionMenu.vue @@ -62,7 +62,8 @@ watch(focused, (focused) => { } }); -const { connectionStore, stepStore } = useWorkflowStores(); +const stores = useWorkflowStores(); +const { stepStore } = stores; interface InputObject { stepId: number; @@ -97,7 +98,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, connectionStore, stepStore); + return terminalFactory(inputObject.stepId, inputSource, props.terminal.datatypesMapper, stores); } const validInputs: ComputedRef = computed(() => { diff --git a/client/src/components/Workflow/Editor/Lint.vue b/client/src/components/Workflow/Editor/Lint.vue index 5ab31d273263..86b80a1c392c 100644 --- a/client/src/components/Workflow/Editor/Lint.vue +++ b/client/src/components/Workflow/Editor/Lint.vue @@ -135,9 +135,10 @@ export default { }, }, setup() { - const { connectionStore, stepStore } = useWorkflowStores(); + const stores = useWorkflowStores(); + const { connectionStore, stepStore } = stores; const { hasActiveOutputs } = storeToRefs(stepStore); - return { connectionStore, stepStore, hasActiveOutputs }; + return { stores, connectionStore, stepStore, hasActiveOutputs }; }, computed: { showRefactor() { @@ -171,7 +172,7 @@ export default { return getUntypedParameters(this.untypedParameters); }, warningDisconnectedInputs() { - return getDisconnectedInputs(this.steps, this.datatypesMapper, this.connectionStore, this.stepStore); + return getDisconnectedInputs(this.steps, this.datatypesMapper, this.stores); }, warningMissingMetadata() { return getMissingMetadata(this.steps); @@ -227,13 +228,7 @@ export default { this.$emit("onUnhighlight", item.stepId); }, onRefactor() { - const actions = fixAllIssues( - this.steps, - this.untypedParameters, - this.datatypesMapper, - this.connectionStore, - this.stepStore - ); + const actions = fixAllIssues(this.steps, this.untypedParameters, this.datatypesMapper, this.stores); this.$emit("onRefactor", actions); }, }, diff --git a/client/src/components/Workflow/Editor/NodeInput.vue b/client/src/components/Workflow/Editor/NodeInput.vue index 81e5dbcd8c1e..b4ab0ac1962b 100644 --- a/client/src/components/Workflow/Editor/NodeInput.vue +++ b/client/src/components/Workflow/Editor/NodeInput.vue @@ -90,7 +90,8 @@ const position = useRelativePosition( computed(() => props.parentNode) ); -const { connectionStore, stateStore, stepStore } = useWorkflowStores(); +const stores = useWorkflowStores(); +const { connectionStore, stateStore } = stores; const hasTerminals = ref(false); watchEffect(() => { hasTerminals.value = connectionStore.getOutputTerminalsForInputTerminal(id.value).length > 0; @@ -178,8 +179,7 @@ function onDrop(event: DragEvent) { stepOut.stepId, stepOut.output, props.datatypesMapper, - connectionStore, - stepStore + stores ) as OutputCollectionTerminal; showTooltip.value = false; diff --git a/client/src/components/Workflow/Editor/NodeOutput.test.ts b/client/src/components/Workflow/Editor/NodeOutput.test.ts index 521822429c3b..9ee9a8d3361a 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 { UndoRedoStore, useUndoRedoStore } from "@/stores/undoRedoStore"; import { useConnectionStore } from "@/stores/workflowConnectionStore"; import { type Step, type Steps, useWorkflowStepStore } from "@/stores/workflowStepStore"; @@ -53,12 +54,14 @@ describe("NodeOutput", () => { let pinia: ReturnType; let stepStore: ReturnType; let connectionStore: ReturnType; + let undoRedoStore: UndoRedoStore; beforeEach(() => { pinia = createPinia(); setActivePinia(pinia); stepStore = useWorkflowStepStore("mock-workflow"); connectionStore = useConnectionStore("mock-workflow"); + undoRedoStore = useUndoRedoStore("mock-workflow"); Object.values(advancedSteps).map((step) => stepStore.addStep(step)); }); @@ -77,20 +80,16 @@ describe("NodeOutput", () => { 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 inputTerminal = terminalFactory(simpleDataStep.id, simpleDataStep.inputs[0]!, testDatatypesMapper, { connectionStore, - stepStore - ); - const outputTerminal = terminalFactory( - listInputStep.id, - listInputStep.outputs[0]!, - testDatatypesMapper, + stepStore, + undoRedoStore, + } as any); + const outputTerminal = terminalFactory(listInputStep.id, listInputStep.outputs[0]!, testDatatypesMapper, { connectionStore, - stepStore - ); + stepStore, + undoRedoStore, + } as any); const propsData = propsForStep(simpleDataStep); const wrapper = shallowMount(NodeOutput as any, { propsData: propsData, diff --git a/client/src/components/Workflow/Editor/NodeOutput.vue b/client/src/components/Workflow/Editor/NodeOutput.vue index af3c571edba6..cac8dba4a5ac 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.vue +++ b/client/src/components/Workflow/Editor/NodeOutput.vue @@ -28,7 +28,7 @@ import { UpdateStepAction } from "./Actions/stepActions"; import { useRelativePosition } from "./composables/relativePosition"; import { useTerminal } from "./composables/useTerminal"; import { type CollectionTypeDescriptor, NULL_COLLECTION_TYPE_DESCRIPTION } from "./modules/collectionTypeDescription"; -import { OutputTerminals } from "./modules/terminals"; +import type { OutputTerminals } from "./modules/terminals"; import DraggableWrapper from "./DraggablePan.vue"; import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue"; diff --git a/client/src/components/Workflow/Editor/composables/useTerminal.ts b/client/src/components/Workflow/Editor/composables/useTerminal.ts index 482c9dfcbe0e..fbe92d11dda3 100644 --- a/client/src/components/Workflow/Editor/composables/useTerminal.ts +++ b/client/src/components/Workflow/Editor/composables/useTerminal.ts @@ -11,21 +11,15 @@ export function useTerminal( datatypesMapper: Ref ) { const terminal: Ref | null> = ref(null); - const { connectionStore, stepStore } = useWorkflowStores(); - const step = computed(() => stepStore.getStep(stepId.value)); - const isMappedOver = computed(() => stepStore.stepMapOver[stepId.value]?.isCollection ?? false); + const stores = useWorkflowStores(); + const step = computed(() => stores.stepStore.getStep(stepId.value)); + const isMappedOver = computed(() => stores.stepStore.stepMapOver[stepId.value]?.isCollection ?? false); watch( [step, terminalSource, datatypesMapper], () => { // rebuild terminal if any of the tracked dependencies change - const newTerminal = terminalFactory( - stepId.value, - terminalSource.value, - datatypesMapper.value, - connectionStore, - stepStore - ); + const newTerminal = terminalFactory(stepId.value, terminalSource.value, datatypesMapper.value, stores); newTerminal.getInvalidConnectedTerminals(); terminal.value = newTerminal; }, diff --git a/client/src/components/Workflow/Editor/modules/linting.ts b/client/src/components/Workflow/Editor/modules/linting.ts index 86f700e70436..b451229305c2 100644 --- a/client/src/components/Workflow/Editor/modules/linting.ts +++ b/client/src/components/Workflow/Editor/modules/linting.ts @@ -1,7 +1,7 @@ import type { DatatypesMapperModel } from "@/components/Datatypes/model"; import type { UntypedParameters } from "@/components/Workflow/Editor/modules/parameters"; -import type { useConnectionStore } from "@/stores/workflowConnectionStore"; -import type { Step, Steps, useWorkflowStepStore } from "@/stores/workflowStepStore"; +import type { useWorkflowStores } from "@/composables/workflowStores"; +import type { Step, Steps } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; import { terminalFactory } from "./terminals"; @@ -18,13 +18,12 @@ interface LintState { export function getDisconnectedInputs( steps: Steps = {}, datatypesMapper: DatatypesMapperModel, - connectionStore: ReturnType, - stepStore: ReturnType + stores: ReturnType ) { const inputs: LintState[] = []; Object.values(steps).forEach((step) => { step.inputs.map((inputSource) => { - const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper, connectionStore, stepStore); + const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper, stores); if (!inputTerminal.optional && inputTerminal.connections.length === 0) { const inputLabel = inputSource.label || inputSource.name; inputs.push({ @@ -124,8 +123,7 @@ export function fixAllIssues( steps: Steps, parameters: UntypedParameters, datatypesMapper: DatatypesMapperModel, - connectionStore: ReturnType, - stepStore: ReturnType + stores: ReturnType ) { const actions = []; const untypedParameters = getUntypedParameters(parameters); @@ -134,7 +132,7 @@ export function fixAllIssues( actions.push(fixUntypedParameter(untypedParameter)); } } - const disconnectedInputs = getDisconnectedInputs(steps, datatypesMapper, connectionStore, stepStore); + const disconnectedInputs = getDisconnectedInputs(steps, datatypesMapper, stores); for (const disconnectedInput of disconnectedInputs) { if (disconnectedInput.autofix) { actions.push(fixDisconnectedInput(disconnectedInput)); diff --git a/client/src/components/Workflow/Editor/modules/terminals.test.ts b/client/src/components/Workflow/Editor/modules/terminals.test.ts index 1c49c1513aa6..844c9114bb14 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.test.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.test.ts @@ -1,7 +1,11 @@ import { createPinia, setActivePinia } from "pinia"; import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; +import { useUndoRedoStore } from "@/stores/undoRedoStore"; import { useConnectionStore } from "@/stores/workflowConnectionStore"; +import { useWorkflowCommentStore } from "@/stores/workflowEditorCommentStore"; +import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; +import { useWorkflowEditorToolbarStore } from "@/stores/workflowEditorToolbarStore"; import { DataOutput, Step, Steps, type TerminalSource, useWorkflowStepStore } from "@/stores/workflowStepStore"; import { advancedSteps, simpleSteps } from "../test_fixtures"; @@ -22,33 +26,38 @@ import { terminalFactory, } from "./terminals"; +function useStores(id = "mock-workflow") { + const connectionStore = useConnectionStore(id); + const stateStore = useWorkflowStateStore(id); + const stepStore = useWorkflowStepStore(id); + const commentStore = useWorkflowCommentStore(id); + const toolbarStore = useWorkflowEditorToolbarStore(id); + const undoRedoStore = useUndoRedoStore(id); + + return { + connectionStore, + stateStore, + stepStore, + commentStore, + toolbarStore, + undoRedoStore, + }; +} + function setupAdvanced() { const terminals: { [index: string]: { [index: string]: ReturnType } } = {}; - const connectionStore = useConnectionStore("mock-workflow"); - const stepStore = useWorkflowStepStore("mock-workflow"); + const stores = useStores(); 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, - connectionStore, - stepStore - ); + terminals[stepLabel]![input.name] = terminalFactory(step.id, input, testDatatypesMapper, stores); }); step.outputs?.map((output) => { - terminals[stepLabel]![output.name] = terminalFactory( - step.id, - output, - testDatatypesMapper, - connectionStore, - stepStore - ); + terminals[stepLabel]![output.name] = terminalFactory(step.id, output, testDatatypesMapper, stores); }); } }); @@ -57,17 +66,16 @@ function setupAdvanced() { function rebuildTerminal>(terminal: T): T { let terminalSource: TerminalSource; - const step = terminal.stepStore.getStep(terminal.stepId); + const step = terminal.stores.stepStore.getStep(terminal.stepId); - const connectionStore = useConnectionStore("mock-workflow"); - const stepStore = useWorkflowStepStore("mock-workflow"); + const stores = useStores(); 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, connectionStore, stepStore) as T; + return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper, stores) as T; } describe("terminalFactory", () => { @@ -103,10 +111,9 @@ describe("terminalFactory", () => { expect(terminals["filter_failed"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); }); it("throws error on invalid terminalSource", () => { - const connectionStore = useConnectionStore("mock-workflow"); - const stepStore = useWorkflowStepStore("mock-workflow"); + const stores = useStores(); - const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper, connectionStore, stepStore); + const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper, stores); expect(invalidFactory).toThrow(); }); }); @@ -560,41 +567,27 @@ describe("canAccept", () => { }); describe("Input terminal", () => { - let stepStore: ReturnType; - let connectionStore: ReturnType; + let stores: ReturnType; let terminals: { [index: number]: { [index: string]: ReturnType } }; beforeEach(() => { setActivePinia(createPinia()); - stepStore = useWorkflowStepStore("mock-workflow"); - connectionStore = useConnectionStore("mock-workflow"); + stores = useStores(); terminals = {}; Object.values(simpleSteps).map((step) => { - stepStore.addStep(step); + stores.stepStore.addStep(step); terminals[step.id] = {}; const stepTerminals = terminals[step.id]!; step.inputs?.map((input) => { - stepTerminals[input.name] = terminalFactory( - step.id, - input, - testDatatypesMapper, - connectionStore, - stepStore - ); + stepTerminals[input.name] = terminalFactory(step.id, input, testDatatypesMapper, stores); }); step.outputs?.map((output) => { - stepTerminals[output.name] = terminalFactory( - step.id, - output, - testDatatypesMapper, - connectionStore, - stepStore - ); + stepTerminals[output.name] = terminalFactory(step.id, output, testDatatypesMapper, stores); }); }); }); it("has step", () => { - expect(stepStore.getStep(1)).toEqual(simpleSteps["1"]); + expect(stores.stepStore.getStep(1)).toEqual(simpleSteps["1"]); }); it("infers correct state", () => { const firstInputTerminal = terminals[1]!["input"] as InputTerminal; @@ -627,11 +620,11 @@ describe("Input terminal", () => { firstInputTerminal.disconnect(connection); expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(true); expect(dataInputOutputTerminal.validInputTerminals().length).toBe(1); - connectionStore.addConnection(connection); + stores.connectionStore.addConnection(connection); expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false); }); it("will maintain invalid connections", () => { - const connection = connectionStore.connections[0]!; + const connection = stores.connectionStore.connections[0]!; connection.output.name = "I don't exist"; const firstInputTerminal = terminals[1]?.["input"] as InputTerminal; const invalidTerminals = firstInputTerminal.getConnectedTerminals(); diff --git a/client/src/components/Workflow/Editor/modules/terminals.ts b/client/src/components/Workflow/Editor/modules/terminals.ts index 0b4745ea7c81..2d85b31dfd52 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.ts @@ -1,12 +1,8 @@ import EventEmitter from "events"; import type { DatatypesMapperModel } from "@/components/Datatypes/model"; -import { - type Connection, - type ConnectionId, - getConnectionId, - type useConnectionStore, -} from "@/stores/workflowConnectionStore"; +import type { useWorkflowStores } from "@/composables/workflowStores"; +import { type Connection, type ConnectionId, getConnectionId } from "@/stores/workflowConnectionStore"; import type { CollectionOutput, DataCollectionStepInput, @@ -15,7 +11,6 @@ import type { ParameterOutput, ParameterStepInput, TerminalSource, - useWorkflowStepStore, } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; @@ -39,8 +34,7 @@ interface BaseTerminalArgs { name: string; stepId: number; datatypesMapper: DatatypesMapperModel; - connectionStore: ReturnType; - stepStore: ReturnType; + stores: ReturnType; } interface InputTerminalInputs { @@ -55,8 +49,7 @@ interface InputTerminalArgs extends BaseTerminalArgs { } class Terminal extends EventEmitter { - connectionStore: ReturnType; - stepStore: ReturnType; + stores; name: string; multiple: boolean; stepId: number; @@ -66,8 +59,7 @@ class Terminal extends EventEmitter { constructor(attr: BaseTerminalArgs) { super(); - this.connectionStore = attr.connectionStore; - this.stepStore = attr.stepStore; + this.stores = attr.stores; this.stepId = attr.stepId; this.name = attr.name; this.multiple = false; @@ -79,19 +71,25 @@ class Terminal extends EventEmitter { return `node-${this.stepId}-${this.terminalType}-${this.name}`; } public get connections(): Connection[] { - return this.connectionStore.getConnectionsForTerminal(this.id); + return this.stores.connectionStore.getConnectionsForTerminal(this.id); } public get mapOver(): CollectionTypeDescriptor { - return this.stepStore.stepMapOver[this.stepId] || NULL_COLLECTION_TYPE_DESCRIPTION; + return this.stores.stepStore.stepMapOver[this.stepId] || NULL_COLLECTION_TYPE_DESCRIPTION; } connect(other: Terminal) { + this.makeConnection(other); + } + makeConnection(other: Terminal) { const connection: Connection = { input: { stepId: this.stepId, name: this.name, connectorType: "input" }, output: { stepId: other.stepId, name: other.name, connectorType: "output" }, }; - this.connectionStore.addConnection(connection); + this.stores.connectionStore.addConnection(connection); } disconnect(other: BaseOutputTerminal | Connection) { + this.dropConnection(other); + } + dropConnection(other: BaseOutputTerminal | Connection) { let connection: Connection; if (other instanceof Terminal) { connection = { @@ -101,7 +99,7 @@ class Terminal extends EventEmitter { } else { connection = other; } - this.connectionStore.removeConnection(getConnectionId(connection)); + this.stores.connectionStore.removeConnection(getConnectionId(connection)); this.resetMappingIfNeeded(connection); } setMapOver(val: CollectionTypeDescriptor) { @@ -120,18 +118,18 @@ class Terminal extends EventEmitter { const effectiveMapOver = this._effectiveMapOver(outputVal); if (!this.localMapOver.equal(effectiveMapOver)) { - this.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver); + this.stores.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver); this.localMapOver = effectiveMapOver; } if ( !this.mapOver.equal(effectiveMapOver) && (effectiveMapOver.isCollection || - !Object.values(this.stepStore.stepInputMapOver[this.stepId] ?? []).find( + !Object.values(this.stores.stepStore.stepInputMapOver[this.stepId] ?? []).find( (mapOver) => mapOver.isCollection )) ) { - this.stepStore.changeStepMapOver(this.stepId, effectiveMapOver); + this.stores.stepStore.changeStepMapOver(this.stepId, effectiveMapOver); } } _effectiveMapOver(otherCollectionType: CollectionTypeDescriptor) { @@ -141,19 +139,20 @@ class Terminal extends EventEmitter { return Boolean(this.mapOver.isCollection); } resetMapping(_connection?: Connection) { - this.stepStore.changeStepMapOver(this.stepId, NULL_COLLECTION_TYPE_DESCRIPTION); - this.stepStore.resetStepInputMapOver(this.stepId); + this.stores.stepStore.changeStepMapOver(this.stepId, NULL_COLLECTION_TYPE_DESCRIPTION); + this.stores.stepStore.resetStepInputMapOver(this.stepId); } hasConnectedMappedInputTerminals() { // check if step has connected and mapped input terminals ... should maybe be on step/node ? - const connections = this.connectionStore.getConnectionsForStep(this.stepId); + const connections = this.stores.connectionStore.getConnectionsForStep(this.stepId); return connections.some( (connection) => - connection.input.stepId === this.stepId && this.stepStore.stepMapOver[this.stepId]?.collectionType + connection.input.stepId === this.stepId && + this.stores.stepStore.stepMapOver[this.stepId]?.collectionType ); } _getOutputConnections() { - return this.connectionStore.getConnectionsForStep(this.stepId).filter((connection) => { + return this.stores.connectionStore.getConnectionsForStep(this.stepId).filter((connection) => { return connection.output.stepId === this.stepId; }); } @@ -162,7 +161,7 @@ class Terminal extends EventEmitter { return this._getOutputConnections().length > 0; } hasMappedOverInputTerminals() { - return Boolean(this.stepStore.stepMapOver[this.stepId]?.collectionType); + return Boolean(this.stores.stepStore.stepMapOver[this.stepId]?.collectionType); } resetMappingIfNeeded(connection?: Connection) { const mapOver = this.mapOver; @@ -189,8 +188,11 @@ class BaseInputTerminal extends Terminal { this.datatypes = attr.input.datatypes; this.multiple = attr.input.multiple; this.optional = attr.input.optional; - if (this.stepStore.stepInputMapOver[this.stepId] && this.stepStore.stepInputMapOver[this.stepId]?.[this.name]) { - this.localMapOver = this.stepStore.stepInputMapOver[this.stepId]![this.name]!; + if ( + this.stores.stepStore.stepInputMapOver[this.stepId] && + this.stores.stepStore.stepInputMapOver[this.stepId]?.[this.name] + ) { + this.localMapOver = this.stores.stepStore.stepInputMapOver[this.stepId]![this.name]!; } else { this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION; } @@ -231,39 +233,29 @@ class BaseInputTerminal extends Terminal { _getOutputStepsMapOver() { const connections = this._getOutputConnections(); const connectedStepIds = Array.from(new Set(connections.map((connection) => connection.output.stepId))); - return connectedStepIds.map((stepId) => this.stepStore.stepMapOver[stepId] || NULL_COLLECTION_TYPE_DESCRIPTION); + return connectedStepIds.map( + (stepId) => this.stores.stepStore.stepMapOver[stepId] || NULL_COLLECTION_TYPE_DESCRIPTION + ); } resetMapping(connection?: Connection) { super.resetMapping(connection); - this.stepStore.changeStepInputMapOver(this.stepId, this.name, NULL_COLLECTION_TYPE_DESCRIPTION); + this.stores.stepStore.changeStepInputMapOver(this.stepId, this.name, NULL_COLLECTION_TYPE_DESCRIPTION); const outputStepIds = this._getOutputTerminals().map((outputTerminal) => outputTerminal.stepId); if (connection) { outputStepIds.push(connection.output.stepId); } Array.from(new Set(outputStepIds)).forEach((stepId) => { - const step = this.stepStore.getStep(stepId); + const step = this.stores.stepStore.getStep(stepId); if (step) { // 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, - this.connectionStore, - this.stepStore - ); + const terminal = terminalFactory(step.id, terminalSource, this.datatypesMapper, this.stores); // drop mapping restrictions terminal.resetMappingIfNeeded(); // re-establish map over through inputs step.inputs.forEach((input) => { - terminalFactory( - step.id, - input, - this.datatypesMapper, - this.connectionStore, - this.stepStore - ).getStepMapOver(); + terminalFactory(step.id, input, this.datatypesMapper, this.stores).getStepMapOver(); }); } } else { @@ -272,7 +264,7 @@ class BaseInputTerminal extends Terminal { }); } _getOutputTerminals() { - return this.connectionStore.getOutputTerminalsForInputTerminal(this.id); + return this.stores.connectionStore.getOutputTerminalsForInputTerminal(this.id); } _getFirstOutputTerminal() { const outputTerminals = this._getOutputTerminals(); @@ -307,7 +299,7 @@ class BaseInputTerminal extends Terminal { _collectionAttached() { const outputTerminals = this._getOutputTerminals(); return outputTerminals.some((outputTerminal) => { - const step = this.stepStore.getStep(outputTerminal.stepId); + const step = this.stores.stepStore.getStep(outputTerminal.stepId); if (!step) { console.error(`Invalid step. Could not find step with id ${outputTerminal.stepId} in store.`); @@ -319,7 +311,7 @@ class BaseInputTerminal extends Terminal { if ( output && (("collection" in output && output.collection) || - this.stepStore.stepMapOver[outputTerminal.stepId]?.isCollection || + this.stores.stepStore.stepMapOver[outputTerminal.stepId]?.isCollection || ("extensions" in output && output.extensions.indexOf("input") > 0)) ) { return true; @@ -361,7 +353,7 @@ class BaseInputTerminal extends Terminal { } getConnectedTerminals() { return this.connections.map((connection) => { - const outputStep = this.stepStore.getStep(connection.output.stepId); + const outputStep = this.stores.stepStore.getStep(connection.output.stepId); if (!outputStep) { return new InvalidOutputTerminal({ stepId: -1, @@ -370,8 +362,7 @@ class BaseInputTerminal extends Terminal { name: connection.output.name, valid: false, datatypesMapper: this.datatypesMapper, - connectionStore: this.connectionStore, - stepStore: this.stepStore, + stores: this.stores, }); } let terminalSource = outputStep.outputs.find((output) => output.name === connection.output.name); @@ -383,8 +374,7 @@ class BaseInputTerminal extends Terminal { name: connection.output.name, valid: false, datatypesMapper: this.datatypesMapper, - connectionStore: this.connectionStore, - stepStore: this.stepStore, + stores: this.stores, }); } const postJobActionKey = `ChangeDatatypeAction${connection.output.name}`; @@ -401,13 +391,7 @@ class BaseInputTerminal extends Terminal { }; } - return terminalFactory( - outputStep.id, - terminalSource, - this.datatypesMapper, - this.connectionStore, - this.stepStore - ); + return terminalFactory(outputStep.id, terminalSource, this.datatypesMapper, this.stores); }); } @@ -416,10 +400,10 @@ class BaseInputTerminal extends Terminal { const canAccept = this.attachable(terminal); const connectionId: ConnectionId = `${this.stepId}-${this.name}-${terminal.stepId}-${terminal.name}`; if (!canAccept.canAccept) { - this.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown"); + this.stores.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown"); return true; - } else if (this.connectionStore.invalidConnections[connectionId]) { - this.connectionStore.dropFromInvalidConnections(connectionId); + } else if (this.stores.connectionStore.invalidConnections[connectionId]) { + this.stores.connectionStore.dropFromInvalidConnections(connectionId); } return false; }); @@ -658,15 +642,15 @@ class BaseOutputTerminal extends Terminal { constructor(attr: BaseOutputTerminalArgs) { super(attr); this.datatypes = attr.datatypes; - this.optional = attr.optional || Boolean(this.stepStore.getStep(this.stepId)?.when); + this.optional = attr.optional || Boolean(this.stores.stepStore.getStep(this.stepId)?.when); this.terminalType = "output"; } getConnectedTerminals(): InputTerminalsAndInvalid[] { return this.connections.map((connection) => { - const inputStep = this.stepStore.getStep(connection.input.stepId); + const inputStep = this.stores.stepStore.getStep(connection.input.stepId); assertDefined(inputStep, `Invalid step. Could not find step with id ${connection.input.stepId} in store.`); - const extraStepInput = this.stepStore.getStepExtraInputs(inputStep.id); + const extraStepInput = this.stores.stepStore.getStepExtraInputs(inputStep.id); const terminalSource = [...extraStepInput, ...inputStep.inputs].find( (input) => input.name === connection.input.name ); @@ -682,17 +666,10 @@ class BaseOutputTerminal extends Terminal { optional: false, multiple: false, }, - connectionStore: this.connectionStore, - stepStore: this.stepStore, + stores: this.stores, }); } - return terminalFactory( - inputStep.id, - terminalSource, - this.datatypesMapper, - this.connectionStore, - this.stepStore - ); + return terminalFactory(inputStep.id, terminalSource, this.datatypesMapper, this.stores); }); } @@ -701,10 +678,10 @@ class BaseOutputTerminal extends Terminal { const canAccept = terminal.attachable(this); const connectionId: ConnectionId = `${terminal.stepId}-${terminal.name}-${this.stepId}-${this.name}`; if (!canAccept.canAccept) { - this.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown"); + this.stores.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown"); return true; - } else if (this.connectionStore.invalidConnections[connectionId]) { - this.connectionStore.dropFromInvalidConnections(connectionId); + } else if (this.stores.connectionStore.invalidConnections[connectionId]) { + this.stores.connectionStore.dropFromInvalidConnections(connectionId); } return false; }); @@ -719,15 +696,9 @@ class BaseOutputTerminal extends Terminal { } validInputTerminals() { const validInputTerminals: InputTerminals[] = []; - Object.values(this.stepStore.steps).map((step) => { + Object.values(this.stores.stepStore.steps).map((step) => { step.inputs?.forEach((input) => { - const inputTerminal = terminalFactory( - step.id, - input, - this.datatypesMapper, - this.connectionStore, - this.stepStore - ); + const inputTerminal = terminalFactory(step.id, input, this.datatypesMapper, this.stores); if (inputTerminal.canAccept(this).canAccept) { validInputTerminals.push(inputTerminal); } @@ -763,13 +734,13 @@ export class OutputCollectionTerminal extends BaseOutputTerminal { } getCollectionTypeFromInput() { - const connection = this.connectionStore.connections.find( + const connection = this.stores.connectionStore.connections.find( (connection) => connection.input.name === this.collectionTypeSource && connection.input.stepId === this.stepId ); if (connection) { - const outputStep = this.stepStore.getStep(connection.output.stepId); - const inputStep = this.stepStore.getStep(this.stepId); + const outputStep = this.stores.stepStore.getStep(connection.output.stepId); + const inputStep = this.stores.stepStore.getStep(this.stepId); assertDefined(inputStep, `Invalid step. Could not find step with id ${connection.input.stepId} in store.`); if (outputStep) { @@ -780,15 +751,13 @@ export class OutputCollectionTerminal extends BaseOutputTerminal { connection.output.stepId, stepOutput, this.datatypesMapper, - this.connectionStore, - this.stepStore + this.stores ); const inputTerminal = terminalFactory( connection.output.stepId, stepInput, this.datatypesMapper, - this.connectionStore, - this.stepStore + this.stores ); // otherCollectionType is the mapped over output collection as it would appear at the input terminal const otherCollectionType = inputTerminal._otherCollectionType(outputTerminal); @@ -936,8 +905,7 @@ export function terminalFactory( stepId: number, terminalSource: T, datatypesMapper: DatatypesMapperModel, - connectionStore: ReturnType, - stepStore: ReturnType + stores: ReturnType ): TerminalOf { if ("input_type" in terminalSource) { const terminalArgs = { @@ -945,8 +913,7 @@ export function terminalFactory( input_type: terminalSource.input_type, name: terminalSource.name, stepId: stepId, - connectionStore, - stepStore, + stores, }; if ("valid" in terminalSource) { return new InvalidInputTerminal({ @@ -994,8 +961,7 @@ export function terminalFactory( optional: terminalSource.optional, stepId: stepId, datatypesMapper: datatypesMapper, - connectionStore, - stepStore, + stores, }; if (isOutputParameterArg(terminalSource)) { return new OutputParameterTerminal({ From 5e2bca29f69aa3e91a25d9ea13826c03ddc9d0b1 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:21:02 +0100 Subject: [PATCH 28/49] add connection undo redo actions --- .../Workflow/Editor/modules/terminals.ts | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/client/src/components/Workflow/Editor/modules/terminals.ts b/client/src/components/Workflow/Editor/modules/terminals.ts index 2d85b31dfd52..5a745b8ac0ba 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.ts @@ -76,20 +76,7 @@ class Terminal extends EventEmitter { public get mapOver(): CollectionTypeDescriptor { return this.stores.stepStore.stepMapOver[this.stepId] || NULL_COLLECTION_TYPE_DESCRIPTION; } - connect(other: Terminal) { - this.makeConnection(other); - } - makeConnection(other: Terminal) { - const connection: Connection = { - input: { stepId: this.stepId, name: this.name, connectorType: "input" }, - output: { stepId: other.stepId, name: other.name, connectorType: "output" }, - }; - this.stores.connectionStore.addConnection(connection); - } - disconnect(other: BaseOutputTerminal | Connection) { - this.dropConnection(other); - } - dropConnection(other: BaseOutputTerminal | Connection) { + buildConnection(other: Terminal | Connection) { let connection: Connection; if (other instanceof Terminal) { connection = { @@ -99,6 +86,28 @@ class Terminal extends EventEmitter { } else { connection = other; } + return connection; + } + connect(other: Terminal | Connection) { + this.stores.undoRedoStore + .action() + .onRun(() => this.makeConnection(other)) + .onUndo(() => this.dropConnection(other)) + .apply(); + } + makeConnection(other: Terminal | Connection) { + const connection = this.buildConnection(other); + this.stores.connectionStore.addConnection(connection); + } + disconnect(other: Terminal | Connection) { + this.stores.undoRedoStore + .action() + .onRun(() => this.dropConnection(other)) + .onUndo(() => this.makeConnection(other)) + .apply(); + } + dropConnection(other: Terminal | Connection) { + const connection = this.buildConnection(other); this.stores.connectionStore.removeConnection(getConnectionId(connection)); this.resetMappingIfNeeded(connection); } From cda8534b2f9d951ead1ac0d4502e9248435c7a2c Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:35:28 +0100 Subject: [PATCH 29/49] fix falsey number bug --- client/src/stores/workflowStepStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index 15cf99b7afe4..8916ac3fdabe 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -208,7 +208,7 @@ export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (work const connectionStore = useConnectionStore(workflowId); function addStep(newStep: NewStep): Step { - const stepId = newStep.id ? newStep.id : getStepIndex.value + 1; + const stepId = newStep.id ?? getStepIndex.value + 1; const step = Object.freeze({ ...newStep, id: stepId } as Step); set(steps.value, stepId.toString(), step); From 133894203fd31235168d455eb5b82af2e056296c Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:44:45 +0100 Subject: [PATCH 30/49] refactor connection construction --- client/src/stores/workflowStepStore.ts | 30 +++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index 8916ac3fdabe..ee49e138ab66 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -381,8 +381,24 @@ export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (work }; }); +function makeConnection(inputId: number, inputName: string, outputId: number, outputName: string): Connection { + return { + input: { + stepId: inputId, + name: inputName, + connectorType: "input", + }, + output: { + stepId: outputId, + name: outputName, + connectorType: "output", + }, + }; +} + function stepToConnections(step: Step): Connection[] { const connections: Connection[] = []; + if (step.input_connections) { Object.entries(step?.input_connections).forEach(([inputName, outputArray]) => { if (outputArray === undefined) { @@ -392,18 +408,7 @@ function stepToConnections(step: Step): Connection[] { outputArray = [outputArray]; } outputArray.forEach((output) => { - const connection: Connection = { - input: { - stepId: step.id, - name: inputName, - connectorType: "input", - }, - output: { - stepId: output.id, - name: output.output_name, - connectorType: "output", - }, - }; + const connection = makeConnection(step.id, inputName, output.id, output.output_name); const connectionInput = step.inputs.find((input) => input.name == inputName); if (connectionInput && "input_subworkflow_step_id" in connectionInput) { connection.input.input_subworkflow_step_id = connectionInput.input_subworkflow_step_id; @@ -412,6 +417,7 @@ function stepToConnections(step: Step): Connection[] { }); }); } + return connections; } From 885f0638db8728abc1405ff941c864680421a969 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:52:24 +0100 Subject: [PATCH 31/49] fix only input connections recreated on remove undo --- .../Workflow/Editor/Actions/stepActions.ts | 14 +++++++++++--- client/src/components/Workflow/Editor/Index.vue | 2 +- client/src/stores/workflowConnectionStore.ts | 2 ++ client/src/stores/workflowStepStore.ts | 8 ++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index afe3fb2d857c..868d3a47868d 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -1,5 +1,6 @@ import { useRefreshFromStore } from "@/stores/refreshFromStore"; import { UndoRedoAction, UndoRedoStore } from "@/stores/undoRedoStore"; +import { Connection, WorkflowConnectionStore } from "@/stores/workflowConnectionStore"; import { WorkflowStateStore } from "@/stores/workflowEditorStateStore"; import type { Step, WorkflowStepStore } from "@/stores/workflowStepStore"; import { assertDefined } from "@/utils/assertions"; @@ -161,20 +162,25 @@ export class InsertStepAction extends UndoRedoAction { export class RemoveStepAction extends UndoRedoAction { stepStore; stateStore; + connectionStore; showAttributesCallback; step: Step; + connections: Connection[]; constructor( stepStore: WorkflowStepStore, stateStore: WorkflowStateStore, + connectionStore: WorkflowConnectionStore, showAttributesCallback: () => void, step: Step ) { super(); this.stepStore = stepStore; this.stateStore = stateStore; + this.connectionStore = connectionStore; this.showAttributesCallback = showAttributesCallback; this.step = structuredClone(step); + this.connections = structuredClone(this.connectionStore.getConnectionsForStep(this.step.id)); } run() { @@ -184,7 +190,8 @@ export class RemoveStepAction extends UndoRedoAction { } undo() { - this.stepStore.addStep(structuredClone(this.step)); + this.stepStore.addStep(structuredClone(this.step), false); + this.connections.forEach((connection) => this.connectionStore.addConnection(connection)); this.stateStore.activeNodeId = this.step.id; this.stateStore.hasChanges = true; } @@ -217,7 +224,8 @@ export class CopyStepAction extends UndoRedoAction { export function useStepActions( stepStore: WorkflowStepStore, undoRedoStore: UndoRedoStore, - stateStore: WorkflowStateStore + stateStore: WorkflowStateStore, + connectionStore: WorkflowConnectionStore ) { /** * If the pending action is a `LazyMutateStepAction` and matches the step id and field key, returns it. @@ -281,7 +289,7 @@ export function useStepActions( } function removeStep(step: Step, showAttributesCallback: () => void) { - const action = new RemoveStepAction(stepStore, stateStore, showAttributesCallback, step); + const action = new RemoveStepAction(stepStore, stateStore, connectionStore, showAttributesCallback, step); undoRedoStore.applyAction(action); } diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 3d348dbe66c3..c6e5c63d5295 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -389,7 +389,7 @@ export default { emit("update:confirmation", false); }); - const stepActions = useStepActions(stepStore, undoRedoStore, stateStore); + const stepActions = useStepActions(stepStore, undoRedoStore, stateStore, connectionStore); return { id, diff --git a/client/src/stores/workflowConnectionStore.ts b/client/src/stores/workflowConnectionStore.ts index 6349abb5f14f..675a53cc253a 100644 --- a/client/src/stores/workflowConnectionStore.ts +++ b/client/src/stores/workflowConnectionStore.ts @@ -89,6 +89,8 @@ export function getConnectionId(item: Connection): ConnectionId { return `${item.input.stepId}-${item.input.name}-${item.output.stepId}-${item.output.name}`; } +export type WorkflowConnectionStore = ReturnType; + export const useConnectionStore = defineScopedStore("workflowConnectionStore", (workflowId) => { const connections = ref[]>([]); const invalidConnections = ref({}); diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index ee49e138ab66..99025538f604 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -207,12 +207,16 @@ export const useWorkflowStepStore = defineScopedStore("workflowStepStore", (work const connectionStore = useConnectionStore(workflowId); - function addStep(newStep: NewStep): Step { + function addStep(newStep: NewStep, createConnections = true): Step { const stepId = newStep.id ?? getStepIndex.value + 1; const step = Object.freeze({ ...newStep, id: stepId } as Step); set(steps.value, stepId.toString(), step); - stepToConnections(step).map((connection) => connectionStore.addConnection(connection)); + + if (createConnections) { + stepToConnections(step).forEach((connection) => connectionStore.addConnection(connection)); + } + stepExtraInputs.value[step.id] = findStepExtraInputs(step); return step; From 6cbae75f51be39949748c896d49364a066036491 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:09:54 +0100 Subject: [PATCH 32/49] name lazy actions as such --- .../Workflow/Editor/Actions/commentActions.ts | 10 +++++----- .../Workflow/Editor/Comments/FrameComment.vue | 6 +++--- .../Editor/Comments/WorkflowComment.vue | 20 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index 49c9b70b3f68..1d1819dd2d3d 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -61,7 +61,7 @@ export class ChangeColorAction extends UndoRedoAction { } } -class MutateCommentAction extends UndoRedoAction { +class LazyMutateCommentAction extends UndoRedoAction { private commentId: number; private startData: WorkflowComment[K]; private endData: WorkflowComment[K]; @@ -95,21 +95,21 @@ class MutateCommentAction extends UndoRedoActio } } -export class ChangeDataAction extends MutateCommentAction<"data"> { +export class LazyChangeDataAction extends LazyMutateCommentAction<"data"> { constructor(store: WorkflowCommentStore, comment: WorkflowComment, data: WorkflowComment["data"]) { const callback = store.changeData; super(comment, "data", data, callback); } } -export class ChangePositionAction extends MutateCommentAction<"position"> { +export class LazyChangePositionAction extends LazyMutateCommentAction<"position"> { constructor(store: WorkflowCommentStore, comment: WorkflowComment, position: [number, number]) { const callback = store.changePosition; super(comment, "position", position, callback); } } -export class ChangeSizeAction extends MutateCommentAction<"size"> { +export class LazyChangeSizeAction extends LazyMutateCommentAction<"size"> { constructor(store: WorkflowCommentStore, comment: WorkflowComment, size: [number, number]) { const callback = store.changeSize; super(comment, "size", size, callback); @@ -118,7 +118,7 @@ export class ChangeSizeAction extends MutateCommentAction<"size"> { type StepWithPosition = Step & { position: NonNullable }; -export class MoveMultipleAction extends UndoRedoAction { +export class LazyMoveMultipleAction extends UndoRedoAction { private commentStore; private stepStore; private comments; diff --git a/client/src/components/Workflow/Editor/Comments/FrameComment.vue b/client/src/components/Workflow/Editor/Comments/FrameComment.vue index 496eb28625a3..9fce95958129 100644 --- a/client/src/components/Workflow/Editor/Comments/FrameComment.vue +++ b/client/src/components/Workflow/Editor/Comments/FrameComment.vue @@ -13,7 +13,7 @@ import { useWorkflowStores } from "@/composables/workflowStores"; import type { FrameWorkflowComment, WorkflowComment, WorkflowCommentColor } from "@/stores/workflowEditorCommentStore"; import type { Step } from "@/stores/workflowStepStore"; -import { MoveMultipleAction } from "../Actions/commentActions"; +import { LazyMoveMultipleAction } from "../Actions/commentActions"; import { brighterColors, darkenedColors } from "./colors"; import { useResizable } from "./useResizable"; import { selectAllText } from "./utilities"; @@ -145,7 +145,7 @@ type StepWithPosition = Step & { position: NonNullable }; let stepsInBounds: StepWithPosition[] = []; let commentsInBounds: WorkflowComment[] = []; -let lazyAction: MoveMultipleAction | null = null; +let lazyAction: LazyMoveMultipleAction | null = null; function getAABB() { const aabb = new AxisAlignedBoundingBox(); @@ -164,7 +164,7 @@ function onDragStart() { commentsInBounds.push(props.comment); - lazyAction = new MoveMultipleAction(commentStore, stepStore, commentsInBounds, stepsInBounds, aabb); + lazyAction = new LazyMoveMultipleAction(commentStore, stepStore, commentsInBounds, stepsInBounds, aabb); undoRedoStore.applyLazyAction(lazyAction); } diff --git a/client/src/components/Workflow/Editor/Comments/WorkflowComment.vue b/client/src/components/Workflow/Editor/Comments/WorkflowComment.vue index 1643b5dfd2d2..1427b3f085de 100644 --- a/client/src/components/Workflow/Editor/Comments/WorkflowComment.vue +++ b/client/src/components/Workflow/Editor/Comments/WorkflowComment.vue @@ -7,10 +7,10 @@ import type { WorkflowComment, WorkflowCommentColor } from "@/stores/workflowEdi import { ChangeColorAction, - ChangeDataAction, - ChangePositionAction, - ChangeSizeAction, DeleteCommentAction, + LazyChangeDataAction, + LazyChangePositionAction, + LazyChangeSizeAction, } from "../Actions/commentActions"; import FrameComment from "./FrameComment.vue"; @@ -37,31 +37,31 @@ const cssVariables = computed(() => ({ })); const { commentStore, undoRedoStore } = useWorkflowStores(); -let lazyAction: ChangeDataAction | ChangePositionAction | ChangeSizeAction | null = null; +let lazyAction: LazyChangeDataAction | LazyChangePositionAction | LazyChangeSizeAction | null = null; function onUpdateData(data: any) { - if (lazyAction instanceof ChangeDataAction && undoRedoStore.isQueued(lazyAction)) { + if (lazyAction instanceof LazyChangeDataAction && undoRedoStore.isQueued(lazyAction)) { lazyAction.updateData(data); } else { - lazyAction = new ChangeDataAction(commentStore, props.comment, data); + lazyAction = new LazyChangeDataAction(commentStore, props.comment, data); undoRedoStore.applyLazyAction(lazyAction); } } function onResize(size: [number, number]) { - if (lazyAction instanceof ChangeSizeAction && undoRedoStore.isQueued(lazyAction)) { + if (lazyAction instanceof LazyChangeSizeAction && undoRedoStore.isQueued(lazyAction)) { lazyAction.updateData(size); } else { - lazyAction = new ChangeSizeAction(commentStore, props.comment, size); + lazyAction = new LazyChangeSizeAction(commentStore, props.comment, size); undoRedoStore.applyLazyAction(lazyAction); } } function onMove(position: [number, number]) { - if (lazyAction instanceof ChangePositionAction && undoRedoStore.isQueued(lazyAction)) { + if (lazyAction instanceof LazyChangePositionAction && undoRedoStore.isQueued(lazyAction)) { lazyAction.updateData(position); } else { - lazyAction = new ChangePositionAction(commentStore, props.comment, position); + lazyAction = new LazyChangePositionAction(commentStore, props.comment, position); undoRedoStore.applyLazyAction(lazyAction); } } From d74aababbf2723ebb52616a44c399facefc3b226 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:23:33 +0100 Subject: [PATCH 33/49] run prettier on readme --- client/src/stores/undoRedoStore/README.md | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/client/src/stores/undoRedoStore/README.md b/client/src/stores/undoRedoStore/README.md index 04287025f481..6155273d0b38 100644 --- a/client/src/stores/undoRedoStore/README.md +++ b/client/src/stores/undoRedoStore/README.md @@ -16,15 +16,15 @@ An example for this may be a tab, or pop-up having it's own separate undo-redo s How undo-redo operations are handled is determined by Undo-Redo actions. There are two ways of creating actions: -* extending the `UndoRedoAction` class, and calling `undoRedoStore.applyAction` -* using the `undoRedoStore.action` factory +- extending the `UndoRedoAction` class, and calling `undoRedoStore.applyAction` +- using the `undoRedoStore.action` factory Actions always provide 4 callbacks, all of them optional: -* run / onRun: ran as soon as the action is applied to the store -* undo / onUndo: ran when an action is rolled back -* redo / onRedo: ran when an action is re-applied. If not defined, `run` will be ran instead -* destroy / onDestroy: ran when an action is discarded, either by the undo stack reaching it's max size, or if a new action is applied when this action is in the redo stack +- run / onRun: ran as soon as the action is applied to the store +- undo / onUndo: ran when an action is rolled back +- redo / onRedo: ran when an action is re-applied. If not defined, `run` will be ran instead +- destroy / onDestroy: ran when an action is discarded, either by the undo stack reaching it's max size, or if a new action is applied when this action is in the redo stack Example: extending the `UndoRedoAction` class: @@ -61,9 +61,10 @@ const commentStore = useWorkflowCommentStore("some-scope"); const newComment = structuredClone(commentStore[comment.id]!); -undoRedoStore.action() +undoRedoStore + .action() .onRun(() => commentStore.deleteComment(newComment.id)) - .onUndo(() => commentStore.addComments([ newComment.id ])) + .onUndo(() => commentStore.addComments([newComment.id])) .apply(); ``` @@ -98,13 +99,9 @@ class ChangeCommentPositionAction extends UndoRedoAction { private startPosition: Position; private endPosition: Position; - constructor( - store: WorkflowCommentStore, - comment: WorkflowComment, - position: Position - ) { + constructor(store: WorkflowCommentStore, comment: WorkflowComment, position: Position) { super(); - this.store + this.store; this.commentId = comment.id; this.startPosition = structuredClone(position); this.endPosition = structuredClone(position); From 68a4dcdd4ca4f594faa3fb28d1ad52fe997fbc20 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:59:20 +0100 Subject: [PATCH 34/49] fix unit test --- .../Workflow/Editor/Actions/commentActions.ts | 2 +- .../Editor/Comments/WorkflowComment.test.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index 1d1819dd2d3d..40a908f03126 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -14,7 +14,7 @@ class CommentAction extends UndoRedoAction { constructor(store: WorkflowCommentStore, comment: BaseWorkflowComment) { super(); this.store = store; - this.comment = structuredClone(this.store.commentsRecord[comment.id]!); + this.comment = structuredClone(comment) as WorkflowComment; } } diff --git a/client/src/components/Workflow/Editor/Comments/WorkflowComment.test.ts b/client/src/components/Workflow/Editor/Comments/WorkflowComment.test.ts index 20d9cf9d01f0..e1056ddf982c 100644 --- a/client/src/components/Workflow/Editor/Comments/WorkflowComment.test.ts +++ b/client/src/components/Workflow/Editor/Comments/WorkflowComment.test.ts @@ -1,6 +1,11 @@ +import { createTestingPinia } from "@pinia/testing"; import { mount, shallowMount } from "@vue/test-utils"; +import { setActivePinia } from "pinia"; import { nextTick } from "vue"; +import { UndoRedoAction } from "@/stores/undoRedoStore"; +import type { TextWorkflowComment } from "@/stores/workflowEditorCommentStore"; + import MarkdownComment from "./MarkdownComment.vue"; import TextComment from "./TextComment.vue"; import WorkflowComment from "./WorkflowComment.vue"; @@ -11,6 +16,9 @@ const changePosition = jest.fn(); const changeColor = jest.fn(); const deleteComment = jest.fn(); +const pinia = createTestingPinia(); +setActivePinia(pinia); + jest.mock("@/composables/workflowStores", () => ({ useWorkflowStores: () => ({ commentStore: { @@ -21,6 +29,10 @@ jest.mock("@/composables/workflowStores", () => ({ deleteComment, isJustCreated: () => false, }, + undoRedoStore: { + applyAction: (action: UndoRedoAction) => action.run(), + applyLazyAction: jest.fn(), + }, }), })); @@ -106,9 +118,11 @@ describe("WorkflowComment", () => { }); it("forwards events to the comment store", () => { + const testComment = { ...comment, id: 123, data: { size: 1, text: "HelloWorld" } } as TextWorkflowComment; + const wrapper = mount(WorkflowComment as any, { propsData: { - comment: { ...comment, id: 123, data: { size: 1, text: "HelloWorld" } }, + comment: testComment, scale: 1, rootOffset: {}, }, From a44ded5807430e678e264f12a7fa8197231d606c Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:47:43 +0100 Subject: [PATCH 35/49] add names to actions --- .../Workflow/Editor/Actions/commentActions.ts | 32 +++++++++++++++++++ .../Workflow/Editor/Actions/stepActions.ts | 20 ++++++++++++ .../Editor/Actions/workflowActions.ts | 4 +++ .../Workflow/Editor/modules/terminals.ts | 2 ++ client/src/stores/undoRedoStore/index.ts | 32 +++++++++++++++++++ .../stores/undoRedoStore/undoRedoAction.ts | 10 ++++++ 6 files changed, 100 insertions(+) diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index 40a908f03126..baaec39d4ea9 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -19,6 +19,10 @@ class CommentAction extends UndoRedoAction { } export class AddCommentAction extends CommentAction { + get name() { + return `add ${this.comment.type} comment`; + } + undo() { this.store.deleteComment(this.comment.id); } @@ -29,6 +33,10 @@ export class AddCommentAction extends CommentAction { } export class DeleteCommentAction extends CommentAction { + get name() { + return `delete ${this.comment.type} comment`; + } + run() { this.store.deleteComment(this.comment.id); } @@ -43,6 +51,7 @@ export class ChangeColorAction extends UndoRedoAction { private toColor: WorkflowCommentColor; private fromColor: WorkflowCommentColor; private store: WorkflowCommentStore; + protected type; constructor(store: WorkflowCommentStore, comment: WorkflowComment, color: WorkflowCommentColor) { super(); @@ -50,6 +59,11 @@ export class ChangeColorAction extends UndoRedoAction { this.commentId = comment.id; this.fromColor = comment.color; this.toColor = color; + this.type = comment.type; + } + + get name() { + return `change ${this.type} comment color to ${this.toColor}`; } run() { @@ -65,6 +79,7 @@ class LazyMutateCommentAction extends UndoRedoA private commentId: number; private startData: WorkflowComment[K]; private endData: WorkflowComment[K]; + protected type; protected applyDataCallback: (commentId: number, data: WorkflowComment[K]) => void; constructor( @@ -79,6 +94,11 @@ class LazyMutateCommentAction extends UndoRedoA this.endData = structuredClone(data); this.applyDataCallback = applyDataCallback; this.applyDataCallback(this.commentId, this.endData); + this.type = comment.type; + } + + get name() { + return `change ${this.type} comment`; } updateData(data: WorkflowComment[K]) { @@ -107,6 +127,10 @@ export class LazyChangePositionAction extends LazyMutateCommentAction<"position" const callback = store.changePosition; super(comment, "position", position, callback); } + + get name() { + return `change ${this.type} comment position`; + } } export class LazyChangeSizeAction extends LazyMutateCommentAction<"size"> { @@ -114,6 +138,10 @@ export class LazyChangeSizeAction extends LazyMutateCommentAction<"size"> { const callback = store.changeSize; super(comment, "size", size, callback); } + + get name() { + return `resize ${this.type} comment`; + } } type StepWithPosition = Step & { position: NonNullable }; @@ -130,6 +158,10 @@ export class LazyMoveMultipleAction extends UndoRedoAction { private positionFrom; private positionTo; + get name() { + return "move multiple nodes"; + } + constructor( commentStore: WorkflowCommentStore, stepStore: WorkflowStepStore, diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index 868d3a47868d..4554e5cfaaba 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -13,6 +13,10 @@ class LazyMutateStepAction extends UndoRedoAction { stepStore; onUndoRedo?: () => void; + get name() { + return "modify step"; + } + constructor(stepStore: WorkflowStepStore, stepId: number, key: K, fromValue: Step[K], toValue: Step[K]) { super(); this.stepStore = stepStore; @@ -48,6 +52,10 @@ export class UpdateStepAction extends UndoRedoAction { toPartial; onUndoRedo?: () => void; + get name() { + return "modify step"; + } + constructor( stepStore: WorkflowStepStore, stateStore: WorkflowStateStore, @@ -128,6 +136,10 @@ export class InsertStepAction extends UndoRedoAction { this.stepData = stepData; } + get name() { + return `insert ${this.stepData.name} step`; + } + stepDataToTuple() { return Object.values(this.stepData) as Parameters; } @@ -183,6 +195,10 @@ export class RemoveStepAction extends UndoRedoAction { this.connections = structuredClone(this.connectionStore.getConnectionsForStep(this.step.id)); } + get name() { + return `remove step ${this.step.label ?? this.step.name}`; + } + run() { this.stepStore.removeStep(this.step.id); this.showAttributesCallback(); @@ -210,6 +226,10 @@ export class CopyStepAction extends UndoRedoAction { this.step = structuredClone(step); } + get name() { + return `duplicate step ${this.step.label ?? this.step.name}`; + } + run() { this.step = this.stepStore.addStep(this.step); this.stateStore.activeNodeId = this.step.id; diff --git a/client/src/components/Workflow/Editor/Actions/workflowActions.ts b/client/src/components/Workflow/Editor/Actions/workflowActions.ts index c00988b68cb8..bf8c29a00751 100644 --- a/client/src/components/Workflow/Editor/Actions/workflowActions.ts +++ b/client/src/components/Workflow/Editor/Actions/workflowActions.ts @@ -30,6 +30,10 @@ export class LazySetValueAction extends UndoRedoAction { this.showAttributesCallback(); this.setValueHandler(this.toValue); } + + get name() { + return "modify workflow"; + } } export class SetValueActionHandler { diff --git a/client/src/components/Workflow/Editor/modules/terminals.ts b/client/src/components/Workflow/Editor/modules/terminals.ts index 5a745b8ac0ba..7ed030b537e2 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.ts +++ b/client/src/components/Workflow/Editor/modules/terminals.ts @@ -93,6 +93,7 @@ class Terminal extends EventEmitter { .action() .onRun(() => this.makeConnection(other)) .onUndo(() => this.dropConnection(other)) + .setName("connect steps") .apply(); } makeConnection(other: Terminal | Connection) { @@ -104,6 +105,7 @@ class Terminal extends EventEmitter { .action() .onRun(() => this.dropConnection(other)) .onUndo(() => this.makeConnection(other)) + .setName("disconnect steps") .apply(); } dropConnection(other: Terminal | Connection) { diff --git a/client/src/stores/undoRedoStore/index.ts b/client/src/stores/undoRedoStore/index.ts index 9e8483b4f926..c80d6c6e5098 100644 --- a/client/src/stores/undoRedoStore/index.ts +++ b/client/src/stores/undoRedoStore/index.ts @@ -114,6 +114,29 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { const isQueued = computed(() => (action?: UndoRedoAction | null) => pendingLazyAction.value === action); + const nextUndoAction = computed(() => undoActionStack.value[undoActionStack.value.length - 1]); + const nextRedoAction = computed(() => redoActionStack.value[redoActionStack.value.length - 1]); + + const undoText = computed(() => { + if (!nextUndoAction.value) { + return "Nothing to undo"; + } else if (!nextUndoAction.value.name) { + return "Undo"; + } else { + return `Undo ${nextUndoAction.value.name}`; + } + }); + + const redoText = computed(() => { + if (!nextRedoAction.value) { + return "Nothing to redo"; + } else if (!nextRedoAction.value.name) { + return "Redo"; + } else { + return `Redo ${nextRedoAction.value.name}`; + } + }); + return { undoActionStack, redoActionStack, @@ -128,6 +151,10 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => { setLazyActionTimeout, isQueued, pendingLazyAction, + nextUndoAction, + nextRedoAction, + undoText, + redoText, $reset, }; }); @@ -165,6 +192,11 @@ class FactoryAction extends UndoRedoAction { return this; } + setName(name: string) { + this.name = name; + return this; + } + apply() { this.applyCallback(this); } diff --git a/client/src/stores/undoRedoStore/undoRedoAction.ts b/client/src/stores/undoRedoStore/undoRedoAction.ts index 022748017a91..58d23dcd423b 100644 --- a/client/src/stores/undoRedoStore/undoRedoAction.ts +++ b/client/src/stores/undoRedoStore/undoRedoAction.ts @@ -1,4 +1,14 @@ export class UndoRedoAction { + private internalName?: string; + + get name(): string | undefined { + return this.internalName; + } + + set name(name: string) { + this.internalName = name; + } + run() { return; } From 6f6809964dd2dd2c0e1b6b8ac3de7654d3a3de2b Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:28:48 +0100 Subject: [PATCH 36/49] add undo/redo buttons --- client/package.json | 1 + .../Workflow/Editor/Actions/stepActions.ts | 4 +-- .../src/components/Workflow/Editor/Index.vue | 28 +++++++++++++++++-- client/src/stores/undoRedoStore/index.ts | 5 ++++ client/yarn.lock | 20 +++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/client/package.json b/client/package.json index f63ed194a64f..15d5b7f818c9 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,7 @@ "@sentry/browser": "^7.74.1", "@types/jest": "^29.5.6", "@vueuse/core": "^10.5.0", + "@vueuse/math": "^10.9.0", "assert": "^2.1.0", "axios": "^1.6.2", "babel-runtime": "^6.26.0", diff --git a/client/src/components/Workflow/Editor/Actions/stepActions.ts b/client/src/components/Workflow/Editor/Actions/stepActions.ts index 4554e5cfaaba..12adf48051d2 100644 --- a/client/src/components/Workflow/Editor/Actions/stepActions.ts +++ b/client/src/components/Workflow/Editor/Actions/stepActions.ts @@ -137,7 +137,7 @@ export class InsertStepAction extends UndoRedoAction { } get name() { - return `insert ${this.stepData.name} step`; + return `insert ${this.stepData.name}`; } stepDataToTuple() { @@ -196,7 +196,7 @@ export class RemoveStepAction extends UndoRedoAction { } get name() { - return `remove step ${this.step.label ?? this.step.name}`; + return `remove ${this.step.label ?? this.step.name}`; } run() { diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index c6e5c63d5295..09d623d6591e 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -44,6 +44,21 @@ Workflow Editor {{ name }} Create New Workflow + + + + + + + + +
-