diff --git a/packages/editor/__tests__/operations/component.spec.ts b/packages/editor/__tests__/operations/component.spec.ts index ee0b38cc..0687898b 100644 --- a/packages/editor/__tests__/operations/component.spec.ts +++ b/packages/editor/__tests__/operations/component.spec.ts @@ -1,19 +1,47 @@ import { AppModel } from '../../src/AppModel/AppModel'; import { ComponentId } from '../../src/AppModel/IAppModel'; import { registry } from '../services'; -import { AppSchema } from './mock'; +import { AppSchema, PasteComponentWithChildrenSchema } from './mock'; import { genOperation } from '../../src/operations'; -describe('Component operations', ()=> { - it('Change component id', ()=> { - const appModel = new AppModel(AppSchema.spec.components, registry); +describe('Component operations', () => { + it('Change component id', () => { + const appModel = new AppModel(AppSchema.spec.components, registry); - expect(appModel.getComponentById('text1' as ComponentId).id).toBe('text1'); - const operation = genOperation(registry, 'modifyComponentId', { - componentId: 'text1', - newId: 'text2', - }); - operation.do(appModel); - expect(appModel.getComponentById('text2' as ComponentId).id).toBe('text2'); - }) -}) + expect(appModel.getComponentById('text1' as ComponentId)?.id).toBe('text1'); + const operation = genOperation(registry, 'modifyComponentId', { + componentId: 'text1', + newId: 'text2', + }); + operation.do(appModel); + expect(appModel.getComponentById('text2' as ComponentId)?.id).toBe('text2'); + }); + + it(`Paste component with children and the children's slot trait is correct`, () => { + const appModel = new AppModel( + PasteComponentWithChildrenSchema.spec.components, + registry + ); + + const pasteComponent = [ + appModel.getComponentById('stack5' as ComponentId)!.toSchema(), + appModel.getComponentById('text6' as ComponentId)!.toSchema(), + ]; + const operation = genOperation(registry, 'pasteComponent', { + parentId: 'stack3', + slot: 'content', + component: new AppModel(pasteComponent, registry).getComponentById( + 'stack5' as ComponentId + )!, + copyTimes: 0, + }); + operation.do(appModel); + expect(appModel.getComponentById('text6_copy0' as ComponentId)?.parent?.id).toBe( + 'stack5_copy0' + ); + expect( + appModel.getComponentById('text6_copy0' as ComponentId)?.traits[0].rawProperties + .container.id + ).toBe('stack5_copy0'); + }); +}); diff --git a/packages/editor/__tests__/operations/mock.ts b/packages/editor/__tests__/operations/mock.ts index 2cc64f36..104f3816 100644 --- a/packages/editor/__tests__/operations/mock.ts +++ b/packages/editor/__tests__/operations/mock.ts @@ -15,3 +15,71 @@ export const AppSchema: Application = { ], }, }; +export const PasteComponentWithChildrenSchema: Application = { + version: 'sunmao/v1', + kind: 'Application', + metadata: { + name: 'some App', + }, + spec: { + components: [ + { + id: 'stack3', + type: 'core/v1/stack', + properties: { + spacing: 12, + direction: 'horizontal', + align: 'auto', + wrap: false, + justify: 'flex-start', + }, + traits: [], + }, + { + id: 'stack5', + type: 'core/v1/stack', + properties: { + spacing: 12, + direction: 'horizontal', + align: 'auto', + wrap: false, + justify: 'flex-start', + }, + traits: [ + { + type: 'core/v1/slot', + properties: { + container: { + id: 'stack3', + slot: 'content', + }, + ifCondition: true, + }, + }, + ], + }, + { + id: 'text6', + type: 'core/v1/text', + properties: { + value: { + raw: 'text', + format: 'plain', + }, + }, + traits: [ + { + type: 'core/v1/slot', + properties: { + container: { + id: 'stack5', + slot: 'content', + }, + ifCondition: true, + }, + }, + ], + }, + ], + }, +}; diff --git a/packages/editor/src/AppModel/AppModel.ts b/packages/editor/src/AppModel/AppModel.ts index efbc27ee..b94e28e0 100644 --- a/packages/editor/src/AppModel/AppModel.ts +++ b/packages/editor/src/AppModel/AppModel.ts @@ -7,11 +7,13 @@ import { ComponentType, IAppModel, IComponentModel, + IFieldModel, ModuleId, SlotName, } from './IAppModel'; import { genComponent } from './utils'; import mitt from 'mitt'; +import { forEach } from 'lodash'; export class AppModel implements IAppModel { topComponents: IComponentModel[] = []; @@ -143,8 +145,35 @@ export class AppModel implements IAppModel { }); } } - this.topComponents.forEach(parent => { - traverse(parent); + if (this.topComponents.length) { + this.topComponents.forEach(parent => { + traverse(parent); + }); + } else { + // When all the components have slot trait, there is no topComponents. + // Just iterate them in order. + Object.values(this.componentMap).forEach(c => { + cb(c); + }); + } + } + + traverseAllFields(cb: (f: IFieldModel) => void) { + function traverseField(field: IFieldModel) { + cb(field); + const value = field.getValue(); + if (typeof value === 'object') { + forEach(value, childField => { + traverseField(childField); + }); + } + } + + this.traverseTree(c => { + traverseField(c.properties); + c.traits.forEach(t => { + traverseField(t.properties); + }); }); } } diff --git a/packages/editor/src/AppModel/ComponentModel.ts b/packages/editor/src/AppModel/ComponentModel.ts index 4dee5790..a34be8a5 100644 --- a/packages/editor/src/AppModel/ComponentModel.ts +++ b/packages/editor/src/AppModel/ComponentModel.ts @@ -68,18 +68,11 @@ export class ComponentModel implements IComponentModel { this.type = schema.type as ComponentType; this.spec = this.registry.getComponentByType(this.type) as any; - this.traits = schema.traits.map( - t => new TraitModel(t, this.registry, this.appModel, this) - ); + this.traits = schema.traits.map(t => new TraitModel(t, this.registry, this)); this.genStateExample(); this.parentId = this._slotTrait?.rawProperties.container.id; this.parentSlot = this._slotTrait?.rawProperties.container.slot; - this.properties = new FieldModel( - schema.properties, - this.spec.spec.properties, - this.appModel, - this - ); + this.properties = new FieldModel(schema.properties, this, this.spec.spec.properties); } get slots() { @@ -174,7 +167,7 @@ export class ComponentModel implements IComponentModel { addTrait(traitType: TraitType, properties: Record): ITraitModel { const traitSchema = genTrait(traitType, properties); - const trait = new TraitModel(traitSchema, this.registry, this.appModel, this); + const trait = new TraitModel(traitSchema, this.registry, this); this.traits.push(trait); this._isDirty = true; this.genStateExample(); @@ -243,7 +236,7 @@ export class ComponentModel implements IComponentModel { } this._isDirty = true; this.appModel.changeComponentMapId(oldId, newId); - this.appModel.emitter.emit('idChange', { oldId, newId }); + this.appModel.traverseAllFields(field => field.changeReferenceId(oldId, newId)); return this; } diff --git a/packages/editor/src/AppModel/FieldModel.ts b/packages/editor/src/AppModel/FieldModel.ts index 8b544ced..6ddbec42 100644 --- a/packages/editor/src/AppModel/FieldModel.ts +++ b/packages/editor/src/AppModel/FieldModel.ts @@ -7,14 +7,12 @@ import { flattenDeep, isArray, isObject } from 'lodash'; import { isExpression } from '../validator/utils'; import { ComponentId, - IAppModel, IComponentModel, ITraitModel, IFieldModel, ModuleId, RefInfo, ASTNode, - AppModelEventType, } from './IAppModel'; import escodegen from 'escodegen'; import { JSONSchema7 } from 'json-schema'; @@ -38,13 +36,11 @@ export class FieldModel implements IFieldModel { constructor( value: unknown, - public spec?: JSONSchema7 & CustomOptions, - private appModel?: IAppModel, private componentModel?: IComponentModel, + public spec?: JSONSchema7 & CustomOptions, private traitModel?: ITraitModel ) { this.update(value); - this.appModel?.emitter.on('idChange', this.onReferenceIdChange.bind(this)); } get rawValue() { @@ -81,11 +77,10 @@ export class FieldModel implements IFieldModel { } else { newValue = new FieldModel( value[key], + this.componentModel, (this.spec?.properties?.[key] || this.spec?.items) as | (JSONSchema7 & CustomOptions) | undefined, - this.appModel, - this.componentModel, this.traitModel ); } @@ -255,7 +250,7 @@ export class FieldModel implements IFieldModel { return path.slice(1).join('.'); } - private onReferenceIdChange({ oldId, newId }: AppModelEventType['idChange']) { + changeReferenceId(oldId: ComponentId, newId: ComponentId) { if (!this.componentModel) { return; } diff --git a/packages/editor/src/AppModel/IAppModel.ts b/packages/editor/src/AppModel/IAppModel.ts index 4c09f903..33fd5f14 100644 --- a/packages/editor/src/AppModel/IAppModel.ts +++ b/packages/editor/src/AppModel/IAppModel.ts @@ -62,6 +62,7 @@ export interface IAppModel { changeComponentMapId(oldId: ComponentId, newId: ComponentId): void; _bindComponentToModel(component: IComponentModel): void; traverseTree(cb: (c: IComponentModel) => void): void; + traverseAllFields(cb: (f: IFieldModel) => void): void; } export interface IModuleModel { @@ -145,4 +146,5 @@ export interface IFieldModel { traverse: (cb: (f: IFieldModel, key: string) => void) => void; // ids of used components in the expression refComponentInfos: Record; + changeReferenceId(oldId: ComponentId, newId: ComponentId): void; } diff --git a/packages/editor/src/AppModel/TraitModel.ts b/packages/editor/src/AppModel/TraitModel.ts index 22d7c080..19d179aa 100644 --- a/packages/editor/src/AppModel/TraitModel.ts +++ b/packages/editor/src/AppModel/TraitModel.ts @@ -6,7 +6,6 @@ import { ITraitModel, IFieldModel, TraitId, - IAppModel, } from './IAppModel'; import { FieldModel } from './FieldModel'; import { genTrait } from './utils'; @@ -24,7 +23,6 @@ export class TraitModel implements ITraitModel { constructor( trait: TraitSchema, private registry: RegistryInterface, - private appModel: IAppModel, public parent: IComponentModel ) { this.schema = trait; @@ -35,9 +33,8 @@ export class TraitModel implements ITraitModel { this.properties = new FieldModel( trait.properties, - this.spec.spec.properties, - this.appModel, this.parent, + this.spec.spec.properties, this ); } diff --git a/packages/editor/src/services/EditorStore.ts b/packages/editor/src/services/EditorStore.ts index 30047aab..cc2b6663 100644 --- a/packages/editor/src/services/EditorStore.ts +++ b/packages/editor/src/services/EditorStore.ts @@ -80,6 +80,7 @@ export class EditorStore { setComponents: action, setHoverComponentId: action, setDragOverComponentId: action, + setHoverComponentId: action, }); this.eventBus.on('selectComponent', id => {