add some operations

This commit is contained in:
Bowen Tan 2021-12-21 14:07:03 +08:00
parent 3a505db5e2
commit 5c59f7d88c
8 changed files with 434 additions and 36 deletions

View File

@ -0,0 +1,168 @@
import { Application, ApplicationComponent } from '@sunmao-ui/core';
import { ApplicationFixture } from '../../__fixture__/application';
import { AdjustComponentOrderLeafOperation } from '../../src/operations/leaf/component/adjustComponentOrderLeafOperation';
import { ApplicationModel } from '../../src/operations/AppModel/AppModel';
import { ComponentType } from '../../src/operations/AppModel/IAppModel';
const AppSchema: Application = {
kind: 'Application',
version: 'example/v1',
metadata: { name: 'dialog_component', description: 'dialog component example' },
spec: {
components: [
{
id: 'hstack1',
type: 'chakra_ui/v1/hstack',
properties: { spacing: '24px' },
traits: [],
},
{
id: 'vstack1',
type: 'chakra_ui/v1/vstack',
properties: {},
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack1', slot: 'content' } },
},
],
},
{
id: 'text1',
type: 'core/v1/text',
properties: { value: { raw: 'text', format: 'plain' } },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack2', slot: 'content' } },
},
],
},
{
id: 'text2',
type: 'core/v1/text',
properties: { value: { raw: 'text', format: 'plain' } },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack2', slot: 'content' } },
},
],
},
{
id: 'button1',
type: 'chakra_ui/v1/button',
properties: {
text: { raw: 'text', format: 'plain' },
colorScheme: 'blue',
isLoading: false,
},
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack2', slot: 'content' } },
},
],
},
{
id: 'hstack2',
type: 'chakra_ui/v1/hstack',
properties: { spacing: '24px', align: '' },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack1', slot: 'content' } },
},
],
},
{
id: 'text3',
type: 'core/v1/text',
properties: { value: { raw: 'VM1', format: 'plain' } },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'vstack1', slot: 'content' } },
},
],
},
{
id: 'text4',
type: 'core/v1/text',
properties: { value: { raw: '虚拟机', format: 'plain' } },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'vstack1', slot: 'content' } },
},
],
},
{
id: 'moduleContainer1',
type: 'core/v1/moduleContainer',
properties: { id: 'myModule', type: 'custom/v1/module' },
traits: [
{
type: 'core/v1/slot',
properties: { container: { id: 'hstack1', slot: 'content' } },
},
],
},
],
},
};
describe('change component properties', () => {
const appModel = new ApplicationModel(AppSchema.spec.components);
const origin = appModel.json;
const text1 = appModel.getComponentById('text1' as any);
text1!.changeComponentProperty('value', { raw: 'hello', format: 'md' });
const newSchema = appModel.json;
expect(newSchema[2].properties.value).toEqual({ raw: 'hello', format: 'md' });
it ('keep immutable after changing component properties', () => {
expect(origin).not.toBe(newSchema);
expect(origin[0]).toBe(newSchema[0]);
expect(origin[1]).toBe(newSchema[1]);
expect(origin[2]).not.toBe(newSchema[2]);
})
});
describe('create component', () => {
const appModel = new ApplicationModel(AppSchema.spec.components);
const origin = appModel.json;
const newComponent = appModel.createComponent('core/v1/text' as ComponentType);
expect(newComponent.id).toEqual('text5');
describe('append component to parent', () => {
const parent = appModel.getComponentById('vstack1' as any)!;
newComponent.appendTo('content' as any, parent);
expect(newComponent.parent).toBe(parent);
expect(newComponent.parentId).toEqual('vstack1');
expect(newComponent.parentSlot).toEqual('content');
it('create slot trait', () => {
expect(newComponent.traits[0].type).toEqual('core/v1/slot');
expect(newComponent.traits[0].properties).toEqual({
container: { id: 'vstack1', slot: 'content' },
});
});
it('update parent children', () => {
expect(parent.children['content' as any]).toContain(newComponent);
expect(newComponent.traits[0].properties).toEqual({
container: { id: 'vstack1', slot: 'content' },
});
});
it('update add model cache', () => {
expect(appModel.allComponents[appModel.allComponents.length - 1]).toBe(newComponent);
});
it ('keep immutable after create component', () => {
const newSchema = appModel.json;
expect(origin).not.toBe(newSchema);
expect(origin.length).toBe(newSchema.length - 1);
expect(origin.every((v, i) => v === newSchema[i])).toBe(true);
const newComponentSchema = newSchema[newSchema.length - 1];
expect(newComponentSchema.id).toBe('text5');
expect(newComponentSchema.traits[0].properties).toEqual({
container: { id: 'vstack1', slot: 'content' },
});
})
});
});

View File

@ -1,4 +1,7 @@
import { ApplicationComponent } from '@sunmao-ui/core';
import { ApplicationComponent, ComponentTrait } from '@sunmao-ui/core';
import { parseType } from '@sunmao-ui/runtime';
import produce from 'immer';
import { registry } from '../../setup';
import { ComponentModel } from './ComponentModel';
import {
ComponentId,
@ -9,22 +12,31 @@ import {
ModuleId,
ModuleType,
SlotName,
TraitType,
} from './IAppModel';
import { genComponent } from './utils';
const SlotTraitType: TraitType = 'core/v1/slot' as TraitType;
export class ApplicationModel implements IApplicationModel {
components: IComponentModel[] = [];
model: IComponentModel[] = [];
modules: IModuleModel[] = [];
allComponents: IComponentModel[] = [];
private origin: ApplicationComponent[] = [];
private schema: ApplicationComponent[] = [];
private componentMap: Record<ComponentId, IComponentModel> = {};
constructor(components: ApplicationComponent[]) {
this.origin = components;
this.schema = components;
this.resolveTree(components);
}
updateSingleComponent(component: IComponentModel) {
this.componentMap[component.id] = component;
this.allComponents.push(component);
}
resolveTree(components: ApplicationComponent[]) {
this.allComponents = components.map(c => {
const comp = new ComponentModel(c);
const comp = new ComponentModel(this, c);
this.componentMap[c.id as ComponentId] = comp;
return comp;
});
@ -41,29 +53,118 @@ export class ApplicationModel implements IApplicationModel {
}
child.parent = parent;
} else {
this.components.push(child);
this.model.push(child);
}
});
}
get json(): ApplicationComponent[] {
return this.origin;
// if (this.allComponents.length !== this.schema.length) {
// return this.allComponents.map(c => c.json);
// }
// if (this.allComponents.some(c => c.isDirty)) {
// return this.allComponents.map(c => c.json);
// }
// for (let i = 0; i < this.schema.length - 1; i++) {
// const comp = this.schema[i];
// const component = this.componentMap[comp.id as ComponentId];
// if (component.isDirty) {
// this.schema.splice(i + 1, 1, component.json);
// }
// }
this.schema = this.allComponents.map(c => {
return c.json;
});
return this.schema;
}
// createComponent (
createComponent(type: ComponentType): IComponentModel {
const component = genComponent(type, this.genId(type), {}, []);
return new ComponentModel(this, component);
}
genId(type: ComponentType): ComponentId {
const { name } = parseType(type);
const componentsCount = this.allComponents.filter(
component => component.type === type
).length;
return `${name}${componentsCount + 1}` as ComponentId;
}
getComponentById(componentId: ComponentId): IComponentModel | undefined {
return this.componentMap[componentId];
}
findComponentIndex(componentId: ComponentId): number {
return this.schema.findIndex(c => c.id === componentId);
}
private updateSchema(schema: ApplicationComponent[]) {
this.schema = schema;
this.model = [];
this.allComponents = [];
this.componentMap = {};
this.resolveTree(schema);
}
// createComponent(
// componentType: ComponentType,
// componentId: ComponentId,
// properties: Record<string, string>
// ) {
// return
// const component = genComponent(componentType, componentId, properties);
// const newSchema = produce(this.schema, draft => {
// draft.push(component);
// });
// this.updateSchema(newSchema);
// return newSchema;
// }
removeComponent(componentId: ComponentId) {
const newSchema = this.schema.filter(c => c.id !== componentId);
this.updateSchema(newSchema);
return newSchema;
}
// createModule: (moduleId: ModuleId, moduleType: ModuleType) => IModuleModel;
// removeComponent: (componentId: ComponentId) => void;
// removeModule: (moduleId: ModuleId) => void;
// findComponent: (componentId: ComponentId) => IComponentModel | undefined;
// moveComponent: (
// fromId: ComponentId,
// toId: ComponentId,
// slot: SlotName,
// afterId: ComponentId
// ) => void;
moveComponent(
fromId: ComponentId,
toId: ComponentId,
slot: SlotName,
afterId?: ComponentId
) {
const fromIndex = this.findComponentIndex(fromId);
const afterIndex = afterId
? this.findComponentIndex(afterId)
: this.findComponentIndex(toId);
this.changeTraitProperties(fromId, SlotTraitType, { container: { id: toId, slot } });
const newSchema = produce(this.schema, draft => {
const target = draft.splice(fromIndex, 1)[0];
draft.splice(fromIndex >= afterIndex ? afterIndex + 1 : afterIndex, 0, target);
});
this.updateSchema(newSchema);
return this.schema;
}
changeTraitProperties(
componentId: ComponentId,
traitType: TraitType,
properties: Record<string, unknown>
) {
const componentIndex = this.findComponentIndex(componentId);
const component = this.schema[componentIndex];
const traitIndex = component?.traits.findIndex(t => t.type === traitType);
if (traitIndex > -1) {
const newSchema = produce(this.schema, draft => {
draft[componentIndex].traits[traitIndex].properties = properties;
});
this.updateSchema(newSchema);
}
return this.schema;
}
}

View File

@ -1,5 +1,6 @@
import { ApplicationComponent, RuntimeComponentSpec } from '@sunmao-ui/core';
import { registry } from '../../setup';
import { genComponent, genTrait, getPropertyObject } from './utils';
import {
ComponentId,
ComponentType,
@ -15,11 +16,13 @@ import {
ITraitModel,
IFieldModel,
EventName,
TraitType,
} from './IAppModel';
import { TraitModel } from './TraitModel';
import { FieldModel } from './FieldModel';
const SlotTraitType: TraitType = 'core/v1/slot' as TraitType;
export class ComponentModel implements IComponentModel {
private origin: ApplicationComponent;
private spec: RuntimeComponentSpec;
id: ComponentId;
@ -30,22 +33,27 @@ export class ComponentModel implements IComponentModel {
parentId: ComponentId | null = null;
parentSlot: SlotName | null = null;
traits: ITraitModel[] = [];
isDirty = false;
constructor(component: ApplicationComponent) {
this.origin = component;
constructor(public appModel: IApplicationModel, private schema: ApplicationComponent) {
this.schema = schema;
this.id = component.id as ComponentId;
this.type = component.type as ComponentType;
this.id = schema.id as ComponentId;
this.type = schema.type as ComponentType;
this.spec = registry.getComponentByType(this.type);
this.traits = component.traits.map(t => new TraitModel(t, this));
this.traits = schema.traits.map(t => new TraitModel(t, this));
// find slot trait
this.traits.forEach(t => {
if (t.type === 'core/v1/slot') {
this.parentId = t.properties.container.id;
this.parentSlot = t.properties.container.slot;
}
})
});
for (const key in schema.properties) {
this.properties[key] = new FieldModel(schema.properties[key]);
}
}
get slots() {
@ -59,7 +67,7 @@ export class ComponentModel implements IComponentModel {
(acc, t) => acc.concat(t.stateKeys),
[] as StateKey[]
);
return [...componentStateKeys, ...traitStateKeys]
return [...componentStateKeys, ...traitStateKeys];
}
get events() {
@ -73,7 +81,7 @@ export class ComponentModel implements IComponentModel {
(acc, t) => acc.concat(t.methods),
[] as MethodName[]
);
return [...componentMethods, ...traitMethods]
return [...componentMethods, ...traitMethods];
}
get styleSlots() {
@ -81,6 +89,67 @@ export class ComponentModel implements IComponentModel {
}
get json(): ApplicationComponent {
return this.origin;
if (!this.isDirty) {
return this.schema;
}
this.isDirty = false;
const newProperties = getPropertyObject(this.properties);
const newTraits = this.traits.map(t => t.json);
const newSchema = genComponent(this.type, this.id, newProperties, newTraits);
this.schema = newSchema;
return this.schema;
// if (this.isDirty || this.traits.length !== this.origin.traits.length) {
// return {
// ...this.origin,
// traits: this.traits.map(t => t.json),
// };
// } else {
// const isChanged = this.traits.some(t => t.isDirty);
// if (isChanged) {
// return {
// ...this.origin,
// traits: this.traits.map(t => t.json),
// };
// }
// }
// return this.origin;
}
changeComponentProperty(propertyName: string, value: any) {
this.properties[propertyName].update(value);
this.isDirty = true;
// const newSchema = produce(this.schema, draft => {
// draft[componentIndex].properties[propertyName] = value;
// });
// this.updateSchema(newSchema)
}
addTrait(traitType: TraitType, properties: Record<string, unknown>): ITraitModel {
const traitSchema = genTrait(traitType, properties);
const trait = new TraitModel(traitSchema, this);
this.traits.push(trait);
this.isDirty = true;
return trait;
}
appendTo = (slot: SlotName, parent: IComponentModel) => {
if (!parent.children[slot]) {
parent.children[slot] = [];
}
// update parent
parent.children[slot].push(this);
this.parent = parent;
this.parentSlot = slot;
this.parentId = parent.id;
// update app model
this.appModel.updateSingleComponent(this);
// update trait
this.addTrait(SlotTraitType, { container: { id: parent.id, slot } });
this.isDirty = true;
};
}

View File

@ -8,6 +8,10 @@ export class FieldModel implements IFieldModel {
// refs: Array<ComponentId | ModuleId> = []
constructor(value: unknown) {
this.update(value);
}
update(value: unknown) {
this.value = value;
this.isDynamic = typeof value === 'string' && regExp.test(value);
}

View File

@ -1,4 +1,4 @@
import { ApplicationComponent } from "@sunmao-ui/core";
import { ApplicationComponent, ComponentTrait } from "@sunmao-ui/core";
export type ComponentId = string & {
kind: 'componentId';
@ -32,16 +32,20 @@ export type EventName = string & {
};
export interface IApplicationModel {
components: IComponentModel[];
model: IComponentModel[];
modules: IModuleModel[];
allComponents: IComponentModel[];
json: ApplicationComponent[];
// createComponent: (componentType: ComponentType, componentId: ComponentId, properties: Record<string, string>) => IComponentModel;
getComponentById(id: ComponentId): IComponentModel | undefined;
genId(type: ComponentType): ComponentId;
// createComponent: (componentType: ComponentType, componentId: ComponentId, properties: Record<string, string>) => ApplicationComponent[];
// createModule: (moduleId: ModuleId, moduleType: ModuleType) => IModuleModel;
// removeComponent: (componentId: ComponentId) => void;
// removeComponent: (componentId: ComponentId) => ApplicationComponent[];
// removeModule: (moduleId: ModuleId) => void;
// findComponent: (componentId: ComponentId) => IComponentModel | undefined;
// findComponent: (componentId: ComponentId) => ApplicationComponent | undefined;
// moveComponent: (fromId: ComponentId, toId: ComponentId, slot: SlotName, afterId: ComponentId) => void;
updateSingleComponent(component: IComponentModel): void;
}
export interface IModuleModel {
@ -51,6 +55,7 @@ export interface IModuleModel {
}
export interface IComponentModel {
appModel: IApplicationModel;
id: ComponentId;
get json (): ApplicationComponent;
type: ComponentType;
@ -65,8 +70,11 @@ export interface IComponentModel {
styleSlots: StyleSlotName[];
methods: MethodName[];
events: EventName[];
isDirty: boolean;
changeComponentProperty: (key: string, value: unknown) => void;
appendTo: (slot: SlotName, parent: IComponentModel) => void;
// addTrait: (traitType: TraitType, properties: Record<string, string>) => ITraitModel;
addTrait: (traitType: TraitType, properties: Record<string, unknown>) => ITraitModel;
// removeTrait: (traitType: TraitType) => void;
// modifyProperty: (propertyName: string, value: any) => void;
// modifyId: (newId: ComponentId) => void;
@ -79,11 +87,14 @@ export interface ITraitModel {
propertiesMedatadata: Record<string, IFieldModel>;
methods: MethodName[];
stateKeys: StateKey[];
isDirty: boolean;
get json (): ComponentTrait;
}
export interface IFieldModel {
value: any;
isDynamic: boolean;
update: (value: any) => void;
// used components' id in the expression
// refs: Array<ComponentId | ModuleId>;
}

View File

@ -12,6 +12,7 @@ import {
StateKey,
} from './IAppModel';
import { FieldModel } from './FieldModel';
import { genTrait, getPropertyObject } from './utils';
export class TraitModel implements ITraitModel {
private origin: ComponentTrait;
@ -21,6 +22,7 @@ export class TraitModel implements ITraitModel {
properties: Record<string, any>;
propertiesMedatadata: Record<string, IFieldModel> = {};
parent: IComponentModel;
isDirty = false
constructor(trait: ComponentTrait, parent: IComponentModel) {
this.origin = trait;
@ -36,7 +38,10 @@ export class TraitModel implements ITraitModel {
}
get json(): ComponentTrait {
return this.origin;
if (!this.isDirty) {
return this.origin;
}
return genTrait(this.type, getPropertyObject(this.properties));
}
get methods() {

View File

@ -0,0 +1,39 @@
import { ApplicationComponent, ComponentTrait } from '@sunmao-ui/core';
import { registry } from '../../setup';
import { FieldModel } from './FieldModel';
export function genComponent(
type: string,
id: string,
properties?: Record<string, unknown>,
traits: ComponentTrait[] = []
): ApplicationComponent {
const cImpl = registry.getComponentByType(type);
const initProperties = properties || cImpl.metadata.exampleProperties;
return {
id,
type: type,
properties: initProperties,
traits,
};
}
export function genTrait(
type: string,
properties: Record<string, unknown> = {}
): ComponentTrait {
return {
type,
properties,
};
}
export function getPropertyObject(
properties: Record<string, FieldModel>
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const key in properties) {
result[key] = properties[key].value;
}
return result;
}

View File

@ -14,14 +14,15 @@ export class AppModelManager implements IUndoRedoManager {
eventBus.on('componentsRefresh', components => {
this.components = components;
this.operationStack = new OperationList();
(window as any).app = new ApplicationModel(this.components);
(window as any).data = this.components;
console.log((window as any).app)
});
}
updateComponents(components: ApplicationComponent[]) {
this.components = components;
eventBus.send('componentsChange', this.components);
(window as any).app = new ApplicationModel(this.components)
console.log((window as any).app)
}
do(operation: IOperation): void {