From c47901b2b7cf1001ea1446569f4a7f68e0b256b8 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 19 Sep 2024 16:31:47 +0400 Subject: [PATCH] Fix undo with pages. Closes #6142 --- packages/core/src/common/index.ts | 4 +- packages/core/src/pages/index.ts | 2 +- packages/core/src/pages/model/Pages.ts | 10 ++-- packages/core/src/undo_manager/index.ts | 17 +++--- packages/core/test/common.ts | 32 +++++++++-- packages/core/test/specs/pages/index.ts | 71 ++++++++++++++++++++++++- 6 files changed, 120 insertions(+), 16 deletions(-) diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 93a15345fb..eb6ec2f9e7 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -14,9 +14,11 @@ export type DisableOptions = { fromMove?: boolean }; export type LocaleOptions = { locale?: boolean }; +export type UndoOptions = { fromUndo?: boolean }; + export type WithHTMLParserOptions = { parserOptions?: HTMLParserOptions }; -export type RemoveOptions = Backbone.Silenceable; +export type RemoveOptions = Backbone.Silenceable & UndoOptions; export type EventHandler = Backbone.EventHandler; diff --git a/packages/core/src/pages/index.ts b/packages/core/src/pages/index.ts index 9b77638209..6945ba0738 100644 --- a/packages/core/src/pages/index.ts +++ b/packages/core/src/pages/index.ts @@ -75,7 +75,7 @@ export default class PageManager extends ItemManagerModule coll.at(0) && this.select(coll.at(0))); this.pages.on('all', this.__onChange, this); diff --git a/packages/core/src/pages/model/Pages.ts b/packages/core/src/pages/model/Pages.ts index a2c54d4ff8..e58da7b1fc 100644 --- a/packages/core/src/pages/model/Pages.ts +++ b/packages/core/src/pages/model/Pages.ts @@ -1,4 +1,4 @@ -import { Collection } from '../../common'; +import { Collection, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; import Page from './Page'; @@ -14,11 +14,13 @@ export default class Pages extends Collection { }; } - onReset(m: Page, opts?: { previousModels?: Pages }) { - opts?.previousModels?.map((p) => this.onRemove(p)); + onReset(m: Page, opts?: RemoveOptions & { previousModels?: Pages }) { + opts?.previousModels?.map((p) => this.onRemove(p, this, opts)); } - onRemove(removed?: Page) { + onRemove(removed?: Page, _p?: this, opts: RemoveOptions = {}) { + // Avoid removing frames if triggered from undo #6142 + if (opts.fromUndo) return; removed?.onRemove(); } } diff --git a/packages/core/src/undo_manager/index.ts b/packages/core/src/undo_manager/index.ts index 0ed345e131..9f2e429901 100644 --- a/packages/core/src/undo_manager/index.ts +++ b/packages/core/src/undo_manager/index.ts @@ -40,6 +40,8 @@ const hasSkip = (opts: any) => opts.avoidStore || opts.noUndo; const getChanged = (obj: any) => Object.keys(obj.changedAttributes()); +const changedMap = new WeakMap(); + export default class UndoManagerModule extends Module { beforeCache?: any; um: any; @@ -69,22 +71,25 @@ export default class UndoManagerModule extends Module { - this.beforeCache = null; + changedMap.delete(object); }); - if (hasSkip(opt)) { return; } else { const after = object.toJSON({ fromUndo }); const result = { object, - before: this.beforeCache, + before, after, }; - this.beforeCache = null; + changedMap.delete(object); // Skip undo in case of empty changes if (isEmpty(after)) return; diff --git a/packages/core/test/common.ts b/packages/core/test/common.ts index 89f54e68bd..0a3ab99c5c 100644 --- a/packages/core/test/common.ts +++ b/packages/core/test/common.ts @@ -1,17 +1,22 @@ +import CanvasEvents from '../src/canvas/types'; import Editor from '../src/editor'; +import { EditorConfig } from '../src/editor/config/config'; +import EditorModel from '../src/editor/model/Editor'; // DocEl + Head + Wrapper export const DEFAULT_CMPS = 3; -export function setupTestEditor() { +export function setupTestEditor(opts?: { withCanvas?: boolean; config?: Partial }) { + document.body.innerHTML = '
'; const editor = new Editor({ mediaCondition: 'max-width', + el: document.body.querySelector('#editor') as HTMLElement, avoidInlineStyle: true, + ...opts?.config, }); const em = editor.getModel(); const dsm = em.DataSources; - document.body.innerHTML = '
'; - const { Pages, Components } = em; + const { Pages, Components, Canvas } = em; Pages.onLoad(); const cmpRoot = Components.getWrapper()!; const View = Components.getType('wrapper')!.view; @@ -22,9 +27,30 @@ export function setupTestEditor() { wrapperEl.render(); const fixtures = document.body.querySelector('#fixtures')!; fixtures.appendChild(wrapperEl.el); + const canvasWrapEl = document.body.querySelector('#canvas-wrp')!; + + /** + * When trying to render the canvas, seems like jest gets stuck in a loop of iframe.onload (FrameView.ts) + * and all subsequent tests containing setTimeout are not executed. + */ + if (opts?.withCanvas) { + Canvas.postLoad(); + canvasWrapEl.appendChild(Canvas.render()); + editor.on(CanvasEvents.frameLoad, ({ el }) => { + // this seems to fix the issue of the loop + el.onload = null; + }); + // Enable undo manager + editor.Pages.postLoad(); + } + return { editor, em, dsm, cmpRoot, fixtures: fixtures as HTMLElement }; } +export function waitEditorEvent(em: Editor | EditorModel, event: string) { + return new Promise((resolve) => em.once(event, resolve)); +} + export function flattenHTML(html: string) { return html.replace(/>\s+|\s+ m.trim()); } diff --git a/packages/core/test/specs/pages/index.ts b/packages/core/test/specs/pages/index.ts index 31da067008..8be5a9e028 100644 --- a/packages/core/test/specs/pages/index.ts +++ b/packages/core/test/specs/pages/index.ts @@ -1,8 +1,9 @@ +import CanvasEvents from '../../../src/canvas/types'; import { ComponentDefinition } from '../../../src/dom_components/model/types'; import Editor from '../../../src/editor'; import EditorModel from '../../../src/editor/model/Editor'; import { PageProperties } from '../../../src/pages/model/Page'; -import { DEFAULT_CMPS } from '../../common'; +import { DEFAULT_CMPS, setupTestEditor, waitEditorEvent } from '../../common'; describe('Pages', () => { let editor: Editor; @@ -281,3 +282,71 @@ describe('Managing pages', () => { expect(rule2.getStyle()).toEqual({ color: 'blue' }); }); }); + +describe('Pages in canvas', () => { + let editor: Editor; + let canvas: Editor['Canvas']; + let em: EditorModel; + let fxt: HTMLElement; + let pm: Editor['Pages']; + const clsPageEl = 'cmp'; + const selPageEl = `.${clsPageEl}`; + + const getPageContent = () => canvas.getBody().querySelector(selPageEl)?.innerHTML; + + beforeEach(async () => { + const testEditor = setupTestEditor({ + withCanvas: true, + config: { + pageManager: { + pages: [ + { + id: 'page-1', + component: `
Page 1
`, + }, + ], + }, + }, + }); + editor = testEditor.editor; + canvas = editor.Canvas; + em = testEditor.em; + fxt = testEditor.fixtures; + pm = editor.Pages; + await waitEditorEvent(em, 'change:readyCanvas'); + }); + + afterEach(() => { + editor.destroy(); + }); + + test('Pages are rendering properly with undo/redo', async () => { + const mainPage = pm.getMain(); + expect(mainPage).toBe(pm.getSelected()); + + const page = pm.add( + { + id: 'page-2', + component: `
Page 2
`, + }, + { select: true }, + )!; + + // Check the second page is selected and rendered properly + expect(page).toBe(pm.getSelected()); + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + expect(getPageContent()).toEqual('Page 2'); + + // Undo and check the main page is rendered properly + em.UndoManager.undo(); + expect(mainPage).toBe(pm.getSelected()); + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + expect(getPageContent()).toBe('Page 1'); + + // Redo and check the second page is rendered properly again + em.UndoManager.redo(); + expect(page).toBe(pm.getSelected()); + await waitEditorEvent(em, CanvasEvents.frameLoadBody); + expect(getPageContent()).toEqual('Page 2'); + }); +});