+
+
showAttributes(true)"
@onHighlight="onHighlight"
@onUnhighlight="onUnhighlight"
@onRefactor="onAttemptRefactor"
@@ -178,7 +186,7 @@
+
diff --git a/client/src/composables/math.ts b/client/src/composables/math.ts
new file mode 100644
index 000000000000..421a9ca24aa5
--- /dev/null
+++ b/client/src/composables/math.ts
@@ -0,0 +1,49 @@
+/**
+ * There are similar functions to those in this module in vue-use, but they only work one-way.
+ * Unlike vue-use, these composables return refs which can be set.
+ */
+
+import { type MaybeRefOrGetter, toValue } from "@vueuse/core";
+import { computed, type Ref } from "vue";
+
+/**
+ * Wraps a number ref, restricting it's values to a given range
+ *
+ * @param ref ref containing a number to wrap
+ * @param min lowest possible value of range
+ * @param max highest possible value of range
+ * @returns clamped ref
+ */
+export function useClamp(ref: Ref, min: MaybeRefOrGetter, max: MaybeRefOrGetter): Ref {
+ const clamp = (value: number) => {
+ return Math.min(Math.max(value, toValue(min)), toValue(max));
+ };
+
+ const clampedRef = computed({
+ get: () => clamp(ref.value),
+ set: (value) => (ref.value = clamp(value)),
+ });
+
+ return clampedRef;
+}
+
+/**
+ * Wraps a number ref, restricting it's values to align to a given step size
+ *
+ * @param ref ref containing a number to wrap
+ * @param [stepSize = 1] size of steps to restrict value to. defaults to 1
+ * @returns wrapped red
+ */
+export function useStep(ref: Ref, stepSize: MaybeRefOrGetter = 1): Ref {
+ const step = (value: number) => {
+ const stepSizeValue = toValue(stepSize);
+ return Math.round(value / stepSizeValue) * stepSizeValue;
+ };
+
+ const steppedRef = computed({
+ get: () => step(ref.value),
+ set: (value) => (ref.value = step(value)),
+ });
+
+ return steppedRef;
+}
diff --git a/client/src/stores/undoRedoStore/index.ts b/client/src/stores/undoRedoStore/index.ts
index da8c354e513f..f83dd6f6d103 100644
--- a/client/src/stores/undoRedoStore/index.ts
+++ b/client/src/stores/undoRedoStore/index.ts
@@ -1,5 +1,7 @@
import { computed, ref } from "vue";
+import { useClamp, useStep } from "@/composables/math";
+import { useUserLocalStorage } from "@/composables/userLocalStorage";
import { defineScopedStore } from "@/stores/scopedStore";
import { type LazyUndoRedoAction, UndoRedoAction } from "./undoRedoAction";
@@ -8,14 +10,34 @@ export { LazyUndoRedoAction, UndoRedoAction } from "./undoRedoAction";
export type UndoRedoStore = ReturnType;
+export class ActionOutOfBoundsError extends Error {
+ public action: UndoRedoAction;
+
+ constructor(action: UndoRedoAction, bounds: "undo" | "redo") {
+ super(`The action "${action.name}" is not in the ${bounds} stack`);
+ this.action = action;
+ }
+}
+
export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => {
const undoActionStack = ref([]);
const redoActionStack = ref([]);
- const maxUndoActions = ref(100);
+
+ const minUndoActions = ref(10);
+ const maxUndoActions = ref(10000);
+
+ const savedUndoActionsValue = useUserLocalStorage(`undoRedoStore-savedUndoActions`, 100);
+ const savedUndoActions = useClamp(useStep(savedUndoActionsValue), minUndoActions, maxUndoActions);
+
+ /** names of actions which were deleted due to savedUndoActions being exceeded */
+ const deletedActions = ref([]);
function $reset() {
undoActionStack.value.forEach((action) => action.destroy());
undoActionStack.value = [];
+ deletedActions.value = [];
+ minUndoActions.value = 10;
+ maxUndoActions.value = 10000;
clearRedoStack();
}
@@ -44,8 +66,9 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => {
clearRedoStack();
undoActionStack.value.push(action);
- while (undoActionStack.value.length > maxUndoActions.value && undoActionStack.value.length > 0) {
+ while (undoActionStack.value.length > savedUndoActions.value && undoActionStack.value.length > 0) {
const action = undoActionStack.value.shift();
+ deletedActions.value.push(action?.name ?? "unnamed action");
action?.destroy();
}
}
@@ -141,10 +164,39 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => {
}
});
+ function rollBackTo(action: UndoRedoAction) {
+ flushLazyAction();
+ const undoSet = new Set(undoActionStack.value);
+
+ if (!undoSet.has(action)) {
+ throw new ActionOutOfBoundsError(action, "undo");
+ }
+
+ while (nextRedoAction.value !== action) {
+ undo();
+ }
+ }
+
+ function rollForwardTo(action: UndoRedoAction) {
+ flushLazyAction();
+ const redoSet = new Set(redoActionStack.value);
+
+ if (!redoSet.has(action)) {
+ throw new ActionOutOfBoundsError(action, "redo");
+ }
+
+ while (nextUndoAction.value !== action) {
+ redo();
+ }
+ }
+
return {
undoActionStack,
redoActionStack,
+ minUndoActions,
maxUndoActions,
+ savedUndoActions,
+ deletedActions,
undo,
redo,
applyAction,
@@ -162,6 +214,8 @@ export const useUndoRedoStore = defineScopedStore("undoRedoStore", () => {
hasUndo,
hasRedo,
$reset,
+ rollBackTo,
+ rollForwardTo,
};
});
diff --git a/client/src/stores/undoRedoStore/undoRedoAction.ts b/client/src/stores/undoRedoStore/undoRedoAction.ts
index 63a54f5e4ff4..88a42a5a0971 100644
--- a/client/src/stores/undoRedoStore/undoRedoAction.ts
+++ b/client/src/stores/undoRedoStore/undoRedoAction.ts
@@ -1,5 +1,12 @@
+let idCounter = 0;
+
export class UndoRedoAction {
protected internalName?: string;
+ public id: number;
+
+ constructor() {
+ this.id = idCounter++;
+ }
get name(): string | undefined {
return this.internalName;
diff --git a/client/src/stores/workflowEditorCommentStore.ts b/client/src/stores/workflowEditorCommentStore.ts
index 3a4ea5301fd6..d3a6c7e1993c 100644
--- a/client/src/stores/workflowEditorCommentStore.ts
+++ b/client/src/stores/workflowEditorCommentStore.ts
@@ -69,6 +69,8 @@ export type WorkflowComment =
| MarkdownWorkflowComment
| FreehandWorkflowComment;
+export type WorkflowCommentType = WorkflowComment["type"];
+
interface CommentsMetadata {
justCreated?: boolean;
multiSelected?: boolean;