fix undo/redo issues (#3658)

Fix some issues with the new undo/redo system - there were a few things
that were undoable that shouldn't be, and a few things that weren't but
should

### Change Type


- [x] `sdk` — Changes the tldraw SDK
- [x] `bugfix` — Bug fix
This commit is contained in:
alex 2024-04-30 12:01:39 +01:00 committed by GitHub
parent 29b6407cdc
commit 8ba46fef49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 108 additions and 9 deletions

View file

@ -56,7 +56,9 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) {
url.searchParams.get(PARAMS.page) ?? 'page:' + url.searchParams.get(PARAMS.p) url.searchParams.get(PARAMS.page) ?? 'page:' + url.searchParams.get(PARAMS.p)
if (newPageId) { if (newPageId) {
if (editor.store.has(newPageId as TLPageId)) { if (editor.store.has(newPageId as TLPageId)) {
editor.setCurrentPage(newPageId as TLPageId) editor.history.ignore(() => {
editor.setCurrentPage(newPageId as TLPageId)
})
} }
} }
} }

View file

@ -1450,15 +1450,18 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public * @public
*/ */
setSelectedShapes(shapes: TLShapeId[] | TLShape[]): this { setSelectedShapes(shapes: TLShapeId[] | TLShape[]): this {
return this.batch(() => { return this.batch(
const ids = shapes.map((shape) => (typeof shape === 'string' ? shape : shape.id)) () => {
const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState() const ids = shapes.map((shape) => (typeof shape === 'string' ? shape : shape.id))
const prevSet = new Set(prevSelectedShapeIds) const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState()
const prevSet = new Set(prevSelectedShapeIds)
if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null
this.store.put([{ ...this.getCurrentPageState(), selectedShapeIds: ids }]) this.store.put([{ ...this.getCurrentPageState(), selectedShapeIds: ids }])
}) },
{ history: 'record-preserveRedoStack' }
)
} }
/** /**
@ -2030,7 +2033,9 @@ export class Editor extends EventEmitter<TLEventMap> {
this.batch(() => { this.batch(() => {
const camera = { ...currentCamera, ...point } const camera = { ...currentCamera, ...point }
this.store.put([camera]) // include id and meta here this.history.ignore(() => {
this.store.put([camera]) // include id and meta here
})
// Dispatch a new pointer move because the pointer's page will have changed // Dispatch a new pointer move because the pointer's page will have changed
// (its screen position will compute to a new page position given the new camera position) // (its screen position will compute to a new page position given the new camera position)

View file

@ -19,3 +19,10 @@ it('centers on the point with animation', () => {
jest.advanceTimersByTime(200) jest.advanceTimersByTime(200)
expect(editor.getViewportPageCenter()).toMatchObject({ x: 400, y: 400 }) expect(editor.getViewportPageCenter()).toMatchObject({ x: 400, y: 400 })
}) })
it('is not undoable', () => {
editor.mark()
editor.centerOnPoint({ x: 400, y: 400 })
editor.undo()
expect(editor.getViewportPageCenter()).toMatchObject({ x: 400, y: 400 })
})

View file

@ -14,6 +14,13 @@ describe('When panning', () => {
editor.expectCameraToBe(200, 200, 1) editor.expectCameraToBe(200, 200, 1)
}) })
it('Is not undoable', () => {
editor.mark()
editor.pan({ x: 200, y: 200 })
editor.undo()
editor.expectCameraToBe(200, 200, 1)
})
it('Updates the pageBounds', () => { it('Updates the pageBounds', () => {
const screenBounds = editor.getViewportScreenBounds() const screenBounds = editor.getViewportScreenBounds()
const beforeScreenBounds = new Box( const beforeScreenBounds = new Box(

View file

@ -27,4 +27,18 @@ describe('When resetting zoom', () => {
editor.resetZoom() editor.resetZoom()
expect(editor.getViewportScreenBounds().center.clone()).toMatchObject(center) expect(editor.getViewportScreenBounds().center.clone()).toMatchObject(center)
}) })
it('is not undoable', () => {
editor.zoomOut()
editor.mark()
editor.resetZoom()
editor.undo()
expect(editor.getZoomLevel()).toBe(1)
editor.mark()
editor.zoomIn()
editor.resetZoom()
editor.undo()
expect(editor.getZoomLevel()).toBe(1)
})
}) })

View file

@ -55,3 +55,29 @@ it('Deleting the parent also deletes descendants', () => {
expect(editor.getShape(ids.box2)).toBeUndefined() expect(editor.getShape(ids.box2)).toBeUndefined()
expect(editor.getShape(ids.ellipse1)).toBeUndefined() expect(editor.getShape(ids.ellipse1)).toBeUndefined()
}) })
it('preserves the redo stack', () => {
editor.mark()
editor.select(ids.box1)
editor.translateSelection(10, 10)
expect(editor.getShape(ids.box1)).toMatchObject({ x: 110, y: 110 })
editor.mark()
editor.translateSelection(10, 10)
expect(editor.getShape(ids.box1)).toMatchObject({ x: 120, y: 120 })
editor.undo()
editor.undo()
expect(editor.getShape(ids.box1)).toMatchObject({ x: 100, y: 100 })
editor.deselect()
editor.redo()
expect(editor.getShape(ids.box1)).toMatchObject({ x: 110, y: 110 })
editor.select(ids.box2)
editor.redo()
expect(editor.getShape(ids.box1)).toMatchObject({ x: 120, y: 120 })
editor.undo()
expect(editor.getShape(ids.box1)).toMatchObject({ x: 110, y: 110 })
})

View file

@ -25,6 +25,13 @@ it('zooms by increments', () => {
expect(editor.getZoomLevel()).toBe(ZOOMS[6]) expect(editor.getZoomLevel()).toBe(ZOOMS[6])
}) })
it('is ignored by undo/redo', () => {
editor.mark()
editor.zoomIn()
editor.undo()
expect(editor.getZoomLevel()).toBe(ZOOMS[4])
})
it('preserves the screen center', () => { it('preserves the screen center', () => {
const viewportCenter = editor.getViewportPageCenter().toJson() const viewportCenter = editor.getViewportPageCenter().toJson()
const screenCenter = editor.getViewportScreenCenter().toJson() const screenCenter = editor.getViewportScreenCenter().toJson()

View file

@ -22,6 +22,13 @@ it('zooms by increments', () => {
expect(editor.getZoomLevel()).toBe(ZOOMS[0]) expect(editor.getZoomLevel()).toBe(ZOOMS[0])
}) })
it('is ignored by undo/redo', () => {
editor.mark()
editor.zoomOut()
editor.undo()
expect(editor.getZoomLevel()).toBe(ZOOMS[2])
})
it('does not zoom out when camera is frozen', () => { it('does not zoom out when camera is frozen', () => {
editor.setCamera({ x: 0, y: 0, z: 1 }) editor.setCamera({ x: 0, y: 0, z: 1 })
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })

View file

@ -48,3 +48,10 @@ it('does not zoom to bounds when camera is frozen', () => {
editor.zoomToBounds(new Box(200, 300, 300, 300)) editor.zoomToBounds(new Box(200, 300, 300, 300))
expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 }) expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 })
}) })
it('is ignored by undo/redo', () => {
editor.mark()
editor.zoomToBounds(new Box(200, 300, 300, 300))
editor.undo()
expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 350, y: 450 })
})

View file

@ -18,3 +18,11 @@ it('does not zoom to bounds when camera is frozen', () => {
editor.zoomToFit() editor.zoomToFit()
expect(editor.getCamera()).toMatchObject(cameraBefore) expect(editor.getCamera()).toMatchObject(cameraBefore)
}) })
it('is ignored by undo/redo', () => {
editor.mark()
editor.zoomToFit()
const camera = editor.getCamera()
editor.undo()
expect(editor.getCamera()).toBe(camera)
})

View file

@ -40,3 +40,12 @@ it('does not zoom to selection when camera is frozen', () => {
editor.zoomToSelection() editor.zoomToSelection()
expect(editor.getCamera()).toMatchObject(cameraBefore) expect(editor.getCamera()).toMatchObject(cameraBefore)
}) })
it('is ignored by undo/redo', () => {
editor.mark()
editor.setSelectedShapes([ids.box1, ids.box2])
editor.zoomToSelection()
const camera = editor.getCamera()
editor.undo()
expect(editor.getCamera()).toBe(camera)
})