mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
fix(undo-redo): eviction policy to not have unbounded undo-redo stacks (#2079)
* fix(undo-redo): eviction policy to not have unbounded undo-redo stacks * fix zindex custom tools delete
This commit is contained in:
committed by
GitHub
parent
f208ff9356
commit
d7586cdd9f
@@ -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)
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -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> }
|
||||
@@ -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') {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -295,6 +361,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
|
||||
newStacks[key] = {
|
||||
undo: stack.undo.slice(-capacity),
|
||||
redo: stack.redo.slice(-capacity),
|
||||
lastUpdated: stack.lastUpdated,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +397,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
|
||||
set({
|
||||
stacks: {
|
||||
...state.stacks,
|
||||
[key]: { undo: validUndo, redo: validRedo },
|
||||
[key]: { ...stack, undo: validUndo, redo: validRedo },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -347,6 +414,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
|
||||
}),
|
||||
{
|
||||
name: 'workflow-undo-redo',
|
||||
storage: createJSONStorage(() => safeStorageAdapter),
|
||||
partialize: (state) => ({
|
||||
stacks: state.stacks,
|
||||
capacity: state.capacity,
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface UndoRedoState {
|
||||
{
|
||||
undo: OperationEntry[]
|
||||
redo: OperationEntry[]
|
||||
lastUpdated?: number
|
||||
}
|
||||
>
|
||||
capacity: number
|
||||
|
||||
Reference in New Issue
Block a user