diff --git a/packages/core/src/canvas/model/Frame.ts b/packages/core/src/canvas/model/Frame.ts index bccbdf630..ab535265e 100644 --- a/packages/core/src/canvas/model/Frame.ts +++ b/packages/core/src/canvas/model/Frame.ts @@ -3,6 +3,7 @@ import CanvasModule from '..'; import { ModuleModel } from '../../abstract'; import { BoxRect, PrevToNewIdMap } from '../../common'; import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; +import { ComponentDefinition } from '../../dom_components/model/types'; import Page from '../../pages/model/Page'; import { createId, isComponent, isObject } from '../../utils/mixins'; import FrameView from '../view/FrameView'; @@ -13,6 +14,21 @@ import CanvasEvents from '../types'; const keyAutoW = '__aw'; const keyAutoH = '__ah'; +export interface FrameProperties { + id?: string; + component?: string | ComponentDefinition | ComponentDefinition[] | ComponentWrapper; + width?: string | number | null; + height?: string | number | null; + x?: number; + y?: number; + attributes?: Record; + head?: { tag: string; attributes: any }[]; + styles?: string | CssRuleJSON[]; + refFrame?: string | Frame | null; + skipFromStorage?: boolean; + [key: string]: unknown; +} + const getDimension = (frame: Frame, type: 'width' | 'height') => { const dim = frame.get(type); const viewDim = frame.view?.el[type === 'width' ? 'offsetWidth' : 'offsetHeight']; @@ -58,7 +74,7 @@ export default class Frame extends ModuleModel { /** * @hideconstructor */ - constructor(module: CanvasModule, attr: any) { + constructor(module: CanvasModule, attr: FrameProperties) { super(module, attr); const { em } = this; const { styles, component } = this.attributes; @@ -254,6 +270,7 @@ export default class Frame extends ModuleModel { const defaults = result(this, 'defaults'); if (opts.fromUndo) delete obj.component; + delete obj.skipFromStorage; delete obj.styles; delete obj.changesCount; obj[keyAutoW] && delete obj.width; diff --git a/packages/core/src/canvas/model/Frames.ts b/packages/core/src/canvas/model/Frames.ts index c0ea2d99e..acd609f8d 100644 --- a/packages/core/src/canvas/model/Frames.ts +++ b/packages/core/src/canvas/model/Frames.ts @@ -36,6 +36,10 @@ export default class Frames extends ModuleCollection { this.forEach((frame) => frame.initRefs()); } + toJSON(opts?: Parameters[0]) { + return this.filter((frame) => !frame.get('skipFromStorage')).map((frame) => frame.toJSON(opts)); + } + itemLoaded() { this.loadedItems++; diff --git a/packages/core/src/pages/index.ts b/packages/core/src/pages/index.ts index 0005063c6..9b7ef1e97 100644 --- a/packages/core/src/pages/index.ts +++ b/packages/core/src/pages/index.ts @@ -276,7 +276,7 @@ export default class PageManager extends ItemManagerModule !page.get('skipFromStorage'))); } load(data: any) { diff --git a/packages/core/src/pages/model/Page.ts b/packages/core/src/pages/model/Page.ts index 03d8f3d39..93acbf126 100644 --- a/packages/core/src/pages/model/Page.ts +++ b/packages/core/src/pages/model/Page.ts @@ -1,5 +1,6 @@ import { forEach, result } from 'underscore'; import { PageManagerConfig } from '../types'; +import type { FrameProperties } from '../../canvas/model/Frame'; import Frames from '../../canvas/model/Frames'; import { Model } from '../../common'; import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; @@ -29,10 +30,20 @@ export interface PageProperties { */ styles?: string | CssRuleJSON[]; + /** + * Frames to load with the page. + */ + frames?: FrameProperties[]; + + /** + * Skip page from project storage. + */ + skipFromStorage?: boolean; + [key: string]: unknown; } -export interface PagePropertiesDefined extends Pick { +export interface PagePropertiesDefined extends Pick { frames: Frames; [key: string]: unknown; } @@ -47,17 +58,17 @@ export default class Page extends Model { } em: EditorModel; - constructor(props: any, opts: { em?: EditorModel; config?: PageManagerConfig } = {}) { - super(props, opts); + constructor(props: PageProperties, opts: { em?: EditorModel; config?: PageManagerConfig } = {}) { + super(props as any, opts); const { em } = opts; - const defFrame: any = {}; + const defFrame: FrameProperties = {}; this.em = em!; if (!props.frames) { defFrame.component = props.component; defFrame.styles = props.styles; ['component', 'styles'].map((i) => this.unset(i)); } - const frms: any[] = props.frames || [defFrame]; + const frms: FrameProperties[] = props.frames || [defFrame]; const frames = new Frames(em!.Canvas, frms); frames.page = this; this.set('frames', frames); @@ -135,6 +146,8 @@ export default class Page extends Model { const obj = Model.prototype.toJSON.call(this, opts); const defaults = result(this, 'defaults'); + delete obj.skipFromStorage; + // Remove private keys forEach(obj, (value, key) => { key.indexOf('_') === 0 && delete obj[key]; diff --git a/packages/core/test/specs/pages/index.ts b/packages/core/test/specs/pages/index.ts index b2abb9a9a..f97778663 100644 --- a/packages/core/test/specs/pages/index.ts +++ b/packages/core/test/specs/pages/index.ts @@ -281,6 +281,131 @@ describe('Managing pages', () => { expect(rule2.getSelectorsString()).toBe(idSel2); expect(rule2.getStyle()).toEqual({ color: 'blue' }); }); + + test('Skip pages from project storage while keeping runtime models', () => { + const storedPage = pm.add({ + id: 'stored-page', + component: '
Stored page
', + })!; + const skippedPage = pm.add({ + id: 'skipped-page', + skipFromStorage: true, + frames: [ + { + id: 'skipped-page-frame', + component: '
Skipped page frame
', + }, + ], + })!; + const storedPage2 = pm.add({ + id: 'stored-page-2', + component: '
Stored page 2
', + })!; + + expect(pm.getAll().map((page) => page.getId())).toEqual([ + pm.getMain().getId(), + storedPage.getId(), + skippedPage.getId(), + storedPage2.getId(), + ]); + expect(skippedPage.getFrames().length).toBe(1); + + const storedPages = editor.getProjectData().pages; + expect(storedPages.map((page: any) => page.id)).toEqual([ + pm.getMain().getId(), + storedPage.getId(), + storedPage2.getId(), + ]); + expect(storedPages.find((page: any) => page.id === skippedPage.getId())).toBeUndefined(); + expect(storedPages.every((page: any) => !('skipFromStorage' in page))).toBe(true); + }); + + test('Skip frames from project storage while keeping runtime models', () => { + const page = pm.add({ + id: 'frames-page', + frames: [ + { + id: 'frame-1', + component: '
Frame 1
', + }, + { + id: 'frame-2', + component: '
Frame 2
', + skipFromStorage: true, + }, + { + id: 'frame-3', + component: '
Frame 3
', + }, + ], + })!; + + expect(page.getFrames().map((frame) => frame.id)).toEqual(['frame-1', 'frame-2', 'frame-3']); + + const storedPage = editor.getProjectData().pages.find((item: any) => item.id === page.getId()); + expect(storedPage.frames.map((frame: any) => frame.id)).toEqual(['frame-1', 'frame-3']); + expect(storedPage.frames.every((frame: any) => !('skipFromStorage' in frame))).toBe(true); + expect(storedPage.frames[0].component).toBeTruthy(); + expect(storedPage.frames[1].component).toBeTruthy(); + }); + + test('Load project data with skipped pages and frames but omit them on the next store', () => { + editor.loadProjectData({ + assets: [], + pages: [ + { + id: 'page-stored', + frames: [ + { + id: 'frame-stored', + component: '
Stored frame
', + }, + { + id: 'frame-skipped', + component: '
Skipped frame
', + skipFromStorage: true, + }, + ], + }, + { + id: 'page-skipped', + skipFromStorage: true, + frames: [ + { + id: 'frame-on-skipped-page', + component: '
Skipped page frame
', + }, + ], + }, + ], + styles: [], + } as any); + + expect(pm.getAll().map((page) => page.getId())).toEqual(['page-stored', 'page-skipped']); + expect(pm.get('page-stored')?.getFrames().map((frame) => frame.id)).toEqual(['frame-stored', 'frame-skipped']); + expect(pm.get('page-skipped')?.getFrames().map((frame) => frame.id)).toEqual(['frame-on-skipped-page']); + + const storedPages = editor.getProjectData().pages; + expect(storedPages.map((page: any) => page.id)).toEqual(['page-stored']); + expect(storedPages[0].frames.map((frame: any) => frame.id)).toEqual(['frame-stored']); + }); + + test('Do not leak skipFromStorage in direct page/frame serialization', () => { + const page = pm.add({ + id: 'serialized-page', + skipFromStorage: false, + frames: [ + { + id: 'serialized-frame', + component: '
Serialized frame
', + skipFromStorage: false, + }, + ], + })!; + + expect(page.toJSON()).not.toHaveProperty('skipFromStorage'); + expect(page.getMainFrame().toJSON()).not.toHaveProperty('skipFromStorage'); + }); }); describe('Pages in canvas', () => {