Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ try {
styleEl.id = styleId
styleEl.textContent = `
[data-radix-portal] [data-radix-dialog-overlay] {
z-index: 99999998 !important;
z-index: 10000048 !important;
}
`
document.head.appendChild(styleEl)
Expand All @@ -934,7 +934,7 @@ try {
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent
className='flex h-[80vh] w-full max-w-[840px] flex-col gap-0 p-0'
style={{ zIndex: 99999999 }}
style={{ zIndex: 10000050 }}
hideCloseButton
onKeyDown={(e) => {
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {
Expand Down
102 changes: 85 additions & 17 deletions apps/sim/stores/undo-redo/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Edge } from 'reactflow'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { createJSONStorage, persist } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockState } from '@/stores/workflows/workflow/types'
import type {
Expand All @@ -14,11 +14,46 @@ import type {

const logger = createLogger('UndoRedoStore')
const DEFAULT_CAPACITY = 100
const MAX_STACKS = 5

function getStackKey(workflowId: string, userId: string): string {
return `${workflowId}:${userId}`
}

/**
* Custom storage adapter for Zustand's persist middleware.
* We need this wrapper to gracefully handle 'QuotaExceededError' when localStorage is full.
* Without this, the default storage engine would throw and crash the application.
*/
const safeStorageAdapter = {
getItem: (name: string): string | null => {
if (typeof localStorage === 'undefined') return null
try {
return localStorage.getItem(name)
} catch (e) {
logger.warn('Failed to read from localStorage', e)
return null
}
},
setItem: (name: string, value: string): void => {
if (typeof localStorage === 'undefined') return
try {
localStorage.setItem(name, value)
} catch (e) {
// Log warning but don't crash - this handles QuotaExceededError
logger.warn('Failed to save to localStorage', e)
}
},
removeItem: (name: string): void => {
if (typeof localStorage === 'undefined') return
try {
localStorage.removeItem(name)
} catch (e) {
logger.warn('Failed to remove from localStorage', e)
}
},
}

function isOperationApplicable(
operation: Operation,
graph: { blocksById: Record<string, BlockState>; edgesById: Record<string, Edge> }
Expand Down Expand Up @@ -73,7 +108,28 @@ export const useUndoRedoStore = create<UndoRedoState>()(
push: (workflowId: string, userId: string, entry: OperationEntry) => {
const key = getStackKey(workflowId, userId)
const state = get()
const stack = state.stacks[key] || { undo: [], redo: [] }
const currentStacks = { ...state.stacks }

// Limit number of stacks
const stackKeys = Object.keys(currentStacks)
if (stackKeys.length >= MAX_STACKS && !currentStacks[key]) {
let oldestKey: string | null = null
let oldestTime = Number.POSITIVE_INFINITY

for (const k of stackKeys) {
const t = currentStacks[k].lastUpdated ?? 0
if (t < oldestTime) {
oldestTime = t
oldestKey = k
}
}

if (oldestKey) {
delete currentStacks[oldestKey]
}
}

const stack = currentStacks[key] || { undo: [], redo: [] }

// Coalesce consecutive move-block operations for the same block
if (entry.operation.type === 'move-block') {
Expand Down Expand Up @@ -137,12 +193,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
return [...stack.undo.slice(0, -1), newEntry]
})()

set({
stacks: {
...state.stacks,
[key]: { undo: newUndoCoalesced, redo: [] },
},
})
currentStacks[key] = {
undo: newUndoCoalesced,
redo: [],
lastUpdated: Date.now(),
}

set({ stacks: currentStacks })

logger.debug('Coalesced consecutive move operations', {
workflowId,
Expand All @@ -160,12 +217,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
newUndo.shift()
}

set({
stacks: {
...state.stacks,
[key]: { undo: newUndo, redo: [] },
},
})
currentStacks[key] = {
undo: newUndo,
redo: [],
lastUpdated: Date.now(),
}

set({ stacks: currentStacks })

logger.debug('Pushed operation to undo stack', {
workflowId,
Expand Down Expand Up @@ -195,7 +253,11 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({
stacks: {
...state.stacks,
[key]: { undo: newUndo, redo: newRedo },
[key]: {
undo: newUndo,
redo: newRedo,
lastUpdated: Date.now(),
},
},
})

Expand Down Expand Up @@ -230,7 +292,11 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({
stacks: {
...state.stacks,
[key]: { undo: newUndo, redo: newRedo },
[key]: {
undo: newUndo,
redo: newRedo,
lastUpdated: Date.now(),
},
},
})

Expand Down Expand Up @@ -295,6 +361,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
newStacks[key] = {
undo: stack.undo.slice(-capacity),
redo: stack.redo.slice(-capacity),
lastUpdated: stack.lastUpdated,
}
}

Expand Down Expand Up @@ -330,7 +397,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({
stacks: {
...state.stacks,
[key]: { undo: validUndo, redo: validRedo },
[key]: { ...stack, undo: validUndo, redo: validRedo },
},
})

Expand All @@ -347,6 +414,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
}),
{
name: 'workflow-undo-redo',
storage: createJSONStorage(() => safeStorageAdapter),
partialize: (state) => ({
stacks: state.stacks,
capacity: state.capacity,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/stores/undo-redo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export interface UndoRedoState {
{
undo: OperationEntry[]
redo: OperationEntry[]
lastUpdated?: number
}
>
capacity: number
Expand Down