mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-18 22:00:22 +08:00
remove editor global instances
This commit is contained in:
parent
509a2585f1
commit
1c6cca1450
@ -1,9 +1,10 @@
|
||||
import { AppModel } from '../../src/AppModel/AppModel';
|
||||
import { ComponentId, ComponentType } from '../../src/AppModel/IAppModel';
|
||||
import { registry } from '../sevices';
|
||||
import { AppSchema, DuplicatedIdSchema } from './mock';
|
||||
|
||||
describe('AppModel test', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
describe('resolve tree', () => {
|
||||
it('resolve Tree corectlly', () => {
|
||||
expect(appModel.allComponents.length).toBe(10);
|
||||
@ -27,7 +28,7 @@ describe('AppModel test', () => {
|
||||
|
||||
it('detect duplicated dd', () => {
|
||||
try {
|
||||
new AppModel(DuplicatedIdSchema);
|
||||
new AppModel(DuplicatedIdSchema, registry);
|
||||
} catch (e: any) {
|
||||
expect(e.message).toBe('Duplicate component id: hstack1');
|
||||
}
|
||||
@ -74,7 +75,7 @@ describe('AppModel test', () => {
|
||||
expect(newComponent.appModel).toBe(appModel);
|
||||
});
|
||||
it('can append component from other appModel', () => {
|
||||
const appModel2 = new AppModel(AppSchema.spec.components);
|
||||
const appModel2 = new AppModel(AppSchema.spec.components, registry);
|
||||
const newComponent2 = appModel2.createComponent('core/v1/text' as ComponentType);
|
||||
expect(newComponent2.appModel).not.toBe(appModel);
|
||||
appModel.appendChild(newComponent2);
|
||||
@ -82,7 +83,7 @@ describe('AppModel test', () => {
|
||||
});
|
||||
|
||||
describe('remove component', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
appModel.removeComponent('text1' as any);
|
||||
it('remove component', () => {
|
||||
|
@ -8,10 +8,11 @@ import {
|
||||
import { AppSchema, EventHanlderMockSchema } from './mock';
|
||||
import { produce } from 'immer';
|
||||
import { get } from 'lodash-es';
|
||||
import { registry } from '../sevices';
|
||||
|
||||
describe('ComponentModel test', () => {
|
||||
it('compute component property', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const button1 = appModel.getComponentById('button1' as ComponentId)!;
|
||||
expect(button1.allComponents.length).toEqual(1);
|
||||
expect(button1.appModel).toEqual(appModel);
|
||||
@ -31,7 +32,7 @@ describe('ComponentModel test', () => {
|
||||
});
|
||||
|
||||
describe('update component property', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const text1 = appModel.getComponentById('text1' as any);
|
||||
text1!.updateComponentProperty('value', { raw: 'hello', format: 'md' });
|
||||
@ -57,7 +58,7 @@ describe('update component property', () => {
|
||||
});
|
||||
|
||||
describe('update event trait handlers(array) property', () => {
|
||||
const appModel = new AppModel(EventHanlderMockSchema);
|
||||
const appModel = new AppModel(EventHanlderMockSchema, registry);
|
||||
const button1 = appModel.getComponentById('button1' as any)!;
|
||||
const oldHandlers = button1.traits[0].rawProperties.handlers;
|
||||
const newHandlers = produce(oldHandlers, (draft: any) => {
|
||||
@ -74,7 +75,7 @@ describe('update event trait handlers(array) property', () => {
|
||||
});
|
||||
|
||||
describe('append to another component', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const newComponent = appModel.createComponent('core/v1/text' as ComponentType);
|
||||
const parent = appModel.getComponentById('vstack1' as any)!;
|
||||
@ -112,7 +113,7 @@ describe('append to another component', () => {
|
||||
});
|
||||
|
||||
describe('append component to top level', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const text1 = appModel.getComponentById('text1' as any)!;
|
||||
it('append component to top level', () => {
|
||||
@ -126,7 +127,7 @@ describe('append to another component', () => {
|
||||
|
||||
describe('append component as child to self', () => {
|
||||
it('append component to top level', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const hstack1 = appModel.getComponentById('hstack1' as any)!;
|
||||
const text1 = appModel.getComponentById('text1' as any)!;
|
||||
@ -138,7 +139,7 @@ describe('append component as child to self', () => {
|
||||
});
|
||||
|
||||
describe('add trait', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const text1 = appModel.getComponentById('text1' as any);
|
||||
text1!.addTrait('core/v1/state' as TraitType, { key: 'value' });
|
||||
@ -156,7 +157,7 @@ describe('add trait', () => {
|
||||
});
|
||||
|
||||
describe('remove trait', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const text1 = appModel.getComponentById('text1' as any)!;
|
||||
text1!.removeTrait(text1.traits[0].id);
|
||||
@ -175,7 +176,7 @@ describe('remove trait', () => {
|
||||
|
||||
describe('change component id', () => {
|
||||
const newId = 'newHstack1' as ComponentId;
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const hStack1 = appModel.getComponentById('hstack1' as ComponentId)!;
|
||||
hStack1.changeId(newId);
|
||||
@ -210,7 +211,7 @@ describe('change component id', () => {
|
||||
});
|
||||
|
||||
describe('move component', () => {
|
||||
const appModel = new AppModel(AppSchema.spec.components);
|
||||
const appModel = new AppModel(AppSchema.spec.components, registry);
|
||||
const origin = appModel.toSchema();
|
||||
const hstack1 = appModel.getComponentById('hstack1' as ComponentId)!;
|
||||
const text1 = appModel.getComponentById('text1' as ComponentId)!;
|
||||
|
4
packages/editor/__tests__/sevices.ts
Normal file
4
packages/editor/__tests__/sevices.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { initSunmaoEditor } from '../src';
|
||||
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
|
||||
export const { registry } = initSunmaoEditor();
|
||||
registry.installLib(sunmaoChakraUILib);
|
@ -1,6 +1,6 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import { OrphanComponentSchema } from './mock';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
import { registry } from '../sevices';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import {
|
||||
ComponentInvalidSchema,
|
||||
ComponentPropertyExpressionSchema,
|
||||
ComponentWrongPropertyExpressionSchema,
|
||||
} from './mock';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
import { registry } from '../sevices';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import {
|
||||
TraitInvalidSchema,
|
||||
EventTraitSchema,
|
||||
EventTraitTraitMethodSchema,
|
||||
} from './mock';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
import { registry } from '../sevices';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ComponentSchema, parseType } from '@sunmao-ui/core';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import { ComponentModel } from './ComponentModel';
|
||||
import {
|
||||
ComponentId,
|
||||
@ -16,7 +17,7 @@ export class AppModel implements IAppModel {
|
||||
private componentMap: Record<ComponentId, IComponentModel> = {};
|
||||
private componentsCount = 0;
|
||||
|
||||
constructor(components: ComponentSchema[]) {
|
||||
constructor(components: ComponentSchema[], private registry: Registry) {
|
||||
this.schema = components;
|
||||
this.componentsCount = components.length;
|
||||
this.resolveTree(components);
|
||||
@ -55,8 +56,8 @@ export class AppModel implements IAppModel {
|
||||
}
|
||||
|
||||
createComponent(type: ComponentType, id?: ComponentId): IComponentModel {
|
||||
const component = genComponent(type, id || this.genId(type));
|
||||
return new ComponentModel(this, component);
|
||||
const component = genComponent(this.registry, type, id || this.genId(type));
|
||||
return new ComponentModel(this, component, this.registry);
|
||||
}
|
||||
|
||||
getComponentById(componentId: ComponentId): IComponentModel | undefined {
|
||||
@ -93,7 +94,7 @@ export class AppModel implements IAppModel {
|
||||
if (this.componentMap[c.id as ComponentId]) {
|
||||
throw new Error(`Duplicate component id: ${c.id}`);
|
||||
} else {
|
||||
const comp = new ComponentModel(this, c);
|
||||
const comp = new ComponentModel(this, c, this.registry);
|
||||
this.componentMap[c.id as ComponentId] = comp;
|
||||
return comp;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { merge } from 'lodash-es';
|
||||
import { parseTypeBox, Registry } from '@sunmao-ui/runtime';
|
||||
import { ComponentSchema, MethodSchema, RuntimeComponent } from '@sunmao-ui/core';
|
||||
import { registry } from '../setup';
|
||||
import { genComponent, genTrait } from './utils';
|
||||
import {
|
||||
ComponentId,
|
||||
@ -17,8 +18,6 @@ import {
|
||||
} from './IAppModel';
|
||||
import { TraitModel } from './TraitModel';
|
||||
import { FieldModel } from './FieldModel';
|
||||
import { merge } from 'lodash-es';
|
||||
import { parseTypeBox } from '@sunmao-ui/runtime';
|
||||
|
||||
const SlotTraitType: TraitType = 'core/v1/slot' as TraitType;
|
||||
|
||||
@ -41,17 +40,21 @@ export class ComponentModel implements IComponentModel {
|
||||
stateExample: Record<string, any> = {};
|
||||
_isDirty = false;
|
||||
|
||||
constructor(public appModel: IAppModel, private schema: ComponentSchema) {
|
||||
constructor(
|
||||
public appModel: IAppModel,
|
||||
private schema: ComponentSchema,
|
||||
private registry: Registry
|
||||
) {
|
||||
this.schema = schema;
|
||||
|
||||
this.id = schema.id as ComponentId;
|
||||
this.type = schema.type as ComponentType;
|
||||
this.spec = registry.getComponentByType(this.type) as any;
|
||||
this.spec = this.registry.getComponentByType(this.type) as any;
|
||||
|
||||
this.traits = schema.traits.map(t => new TraitModel(t, this));
|
||||
this.genStateExample()
|
||||
this.traits = schema.traits.map(t => new TraitModel(t, this, this.registry));
|
||||
this.genStateExample();
|
||||
this.parentId = this._slotTrait?.rawProperties.container.id;
|
||||
this.parentSlot = this._slotTrait?.rawProperties.container.slot;
|
||||
this.parentSlot = this._slotTrait?.rawProperties.container.slot;
|
||||
this.properties = new FieldModel(schema.properties);
|
||||
}
|
||||
|
||||
@ -120,7 +123,7 @@ export class ComponentModel implements IComponentModel {
|
||||
this._isDirty = false;
|
||||
const newProperties = this.rawProperties;
|
||||
const newTraits = this.traits.map(t => t.toSchema());
|
||||
const newSchema = genComponent(this.type, this.id, newProperties, newTraits);
|
||||
const newSchema = genComponent(this.registry, this.type, this.id, newProperties, newTraits);
|
||||
this.schema = newSchema;
|
||||
}
|
||||
return this.schema;
|
||||
@ -133,10 +136,10 @@ export class ComponentModel implements IComponentModel {
|
||||
|
||||
addTrait(traitType: TraitType, properties: Record<string, unknown>): ITraitModel {
|
||||
const traitSchema = genTrait(traitType, properties);
|
||||
const trait = new TraitModel(traitSchema, this);
|
||||
const trait = new TraitModel(traitSchema, this, this.registry);
|
||||
this.traits.push(trait);
|
||||
this._isDirty = true;
|
||||
this.genStateExample()
|
||||
this.genStateExample();
|
||||
return trait;
|
||||
}
|
||||
|
||||
@ -178,7 +181,7 @@ export class ComponentModel implements IComponentModel {
|
||||
if (traitIndex === -1) return;
|
||||
this.traits.splice(traitIndex, 1);
|
||||
this._isDirty = true;
|
||||
this.genStateExample()
|
||||
this.genStateExample();
|
||||
}
|
||||
|
||||
changeId(newId: ComponentId) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TraitSchema, RuntimeTrait } from '@sunmao-ui/core';
|
||||
import { registry } from '../setup';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import {
|
||||
IComponentModel,
|
||||
TraitType,
|
||||
@ -20,22 +20,26 @@ export class TraitModel implements ITraitModel {
|
||||
properties: IFieldModel;
|
||||
_isDirty = false;
|
||||
|
||||
constructor(trait: TraitSchema, public parent: IComponentModel) {
|
||||
constructor(
|
||||
trait: TraitSchema,
|
||||
public parent: IComponentModel,
|
||||
private registry: Registry
|
||||
) {
|
||||
this.schema = trait;
|
||||
this.parent = parent;
|
||||
this.type = trait.type as TraitType;
|
||||
this.id = `${this.parent.id}_trait${traitIdCount++}` as TraitId;
|
||||
this.spec = registry.getTraitByType(this.type);
|
||||
this.spec = this.registry.getTraitByType(this.type);
|
||||
|
||||
this.properties = new FieldModel(trait.properties);
|
||||
this.properties = new FieldModel(trait.properties);
|
||||
}
|
||||
|
||||
get rawProperties() {
|
||||
return this.properties.rawValue
|
||||
return this.properties.rawValue;
|
||||
}
|
||||
|
||||
get methods() {
|
||||
return this.spec ? this.spec.spec.methods : []
|
||||
return this.spec ? this.spec.spec.methods : [];
|
||||
}
|
||||
|
||||
toSchema(): TraitSchema {
|
||||
@ -46,7 +50,7 @@ export class TraitModel implements ITraitModel {
|
||||
}
|
||||
|
||||
updateProperty(key: string, value: any) {
|
||||
this.properties.update({[key]: value})
|
||||
this.properties.update({ [key]: value });
|
||||
this._isDirty = true;
|
||||
this.parent._isDirty = true;
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { ComponentSchema, TraitSchema } from '@sunmao-ui/core';
|
||||
import { registry } from '../setup';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
|
||||
export function genComponent(
|
||||
registry: Registry,
|
||||
type: string,
|
||||
id: string,
|
||||
properties?: Record<string, unknown>,
|
||||
traits: TraitSchema[] = []
|
||||
traits: TraitSchema[] = [],
|
||||
): ComponentSchema {
|
||||
const cImpl = registry.getComponentByType(type);
|
||||
const initProperties = properties || cImpl.metadata.exampleProperties;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { action, makeAutoObservable, observable, reaction, toJS } from 'mobx';
|
||||
import { ComponentSchema, createModule } from '@sunmao-ui/core';
|
||||
import { eventBus } from './eventBus';
|
||||
import { Registry, StateManager } from '@sunmao-ui/runtime';
|
||||
|
||||
import { EventBusType } from './eventBus';
|
||||
import { AppStorage } from './AppStorage';
|
||||
import { registry, stateManager } from './setup';
|
||||
import { SchemaValidator } from './validator';
|
||||
import { addModuleId } from './utils/addModuleId';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
type EditingTarget = {
|
||||
kind: 'app' | 'module';
|
||||
@ -13,7 +14,7 @@ type EditingTarget = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
class EditorStore {
|
||||
export class EditorStore {
|
||||
components: ComponentSchema[] = [];
|
||||
// currentEditingComponents, it could be app's or module's components
|
||||
_selectedComponentId = '';
|
||||
@ -30,8 +31,49 @@ class EditorStore {
|
||||
lastSavedComponentsVersion = 0;
|
||||
|
||||
appStorage = new AppStorage();
|
||||
schemaValidator = new SchemaValidator(registry);
|
||||
schemaValidator: SchemaValidator;
|
||||
|
||||
constructor(
|
||||
private eventBus: EventBusType,
|
||||
private registry: Registry,
|
||||
private stateManager: StateManager
|
||||
) {
|
||||
this.schemaValidator = new SchemaValidator(this.registry)
|
||||
makeAutoObservable(this, {
|
||||
components: observable.shallow,
|
||||
setComponents: action,
|
||||
setDragOverComponentId: action,
|
||||
});
|
||||
|
||||
this.eventBus.on('selectComponent', id => {
|
||||
this.setSelectedComponentId(id);
|
||||
});
|
||||
// listen the change by operations, and save newComponents
|
||||
this.eventBus.on('componentsChange', components => {
|
||||
this.setComponents(components);
|
||||
this.setCurrentComponentsVersion(this.currentComponentsVersion + 1);
|
||||
|
||||
if (this.validateResult.length === 0) {
|
||||
this.saveCurrentComponents();
|
||||
}
|
||||
});
|
||||
|
||||
// when switch app or module, components should refresh
|
||||
reaction(
|
||||
() => this.currentEditingTarget,
|
||||
target => {
|
||||
if (target.name) {
|
||||
this.setCurrentComponentsVersion(0);
|
||||
this.setLastSavedComponentsVersion(0);
|
||||
this.clearSunmaoGlobalState();
|
||||
this.eventBus.send('componentsRefresh', this.originComponents);
|
||||
this.setComponents(this.originComponents);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
|
||||
}
|
||||
get app() {
|
||||
return this.appStorage.app;
|
||||
}
|
||||
@ -65,43 +107,6 @@ class EditorStore {
|
||||
return this.currentComponentsVersion === this.lastSavedComponentsVersion;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
components: observable.shallow,
|
||||
setComponents: action,
|
||||
setDragOverComponentId: action,
|
||||
});
|
||||
|
||||
eventBus.on('selectComponent', id => {
|
||||
this.setSelectedComponentId(id);
|
||||
});
|
||||
// listen the change by operations, and save newComponents
|
||||
eventBus.on('componentsChange', components => {
|
||||
this.setComponents(components);
|
||||
this.setCurrentComponentsVersion(this.currentComponentsVersion + 1);
|
||||
|
||||
if (this.validateResult.length === 0) {
|
||||
this.saveCurrentComponents();
|
||||
}
|
||||
});
|
||||
|
||||
// when switch app or module, components should refresh
|
||||
reaction(
|
||||
() => this.currentEditingTarget,
|
||||
target => {
|
||||
if (target.name) {
|
||||
this.setCurrentComponentsVersion(0);
|
||||
this.setLastSavedComponentsVersion(0);
|
||||
this.clearSunmaoGlobalState();
|
||||
eventBus.send('componentsRefresh', this.originComponents);
|
||||
this.setComponents(this.originComponents);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
|
||||
}
|
||||
|
||||
// origin components of app of module
|
||||
// when switch app or module, components should refresh
|
||||
get originComponents(): ComponentSchema[] {
|
||||
@ -119,11 +124,11 @@ class EditorStore {
|
||||
}
|
||||
|
||||
clearSunmaoGlobalState() {
|
||||
stateManager.clear();
|
||||
this.stateManager.clear();
|
||||
// reregister all modules
|
||||
this.modules.forEach(m => {
|
||||
const modules = createModule(addModuleId(cloneDeep(m)));
|
||||
registry.registerModule(modules, true);
|
||||
this.registry.registerModule(modules, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -167,5 +172,3 @@ class EditorStore {
|
||||
this.lastSavedComponentsVersion = val;
|
||||
};
|
||||
}
|
||||
|
||||
export const editorStore = new EditorStore();
|
||||
|
@ -1,23 +1,22 @@
|
||||
import React from 'react';
|
||||
import { flatten } from 'lodash-es';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FormControl, FormLabel, Input, Textarea, VStack } from '@chakra-ui/react';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import { parseType } from '@sunmao-ui/core';
|
||||
import { parseTypeBox } from '@sunmao-ui/runtime';
|
||||
import { eventBus } from '../../eventBus';
|
||||
|
||||
import { EventTraitForm } from './EventTraitForm';
|
||||
import { GeneralTraitFormList } from './GeneralTraitFormList';
|
||||
import { FetchTraitForm } from './FetchTraitForm';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import SchemaField from './JsonSchemaForm/SchemaField';
|
||||
import { genOperation } from '../../operations';
|
||||
import { editorStore } from '../../EditorStore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { StyleTraitForm } from './StyleTraitForm';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const renderField = (properties: {
|
||||
@ -27,20 +26,22 @@ export const renderField = (properties: {
|
||||
fullKey: string;
|
||||
selectedComponentId: string;
|
||||
index: number;
|
||||
services: EditorServices;
|
||||
}) => {
|
||||
const { value, type, fullKey, selectedComponentId, index } = properties;
|
||||
const { value, type, fullKey, selectedComponentId, index, services } = properties;
|
||||
const { eventBus, registry } = services;
|
||||
if (typeof value !== 'object') {
|
||||
const ref = React.createRef<HTMLTextAreaElement>();
|
||||
const onBlur = () => {
|
||||
const operation = type
|
||||
? genOperation('modifyTraitProperty', {
|
||||
? genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: selectedComponentId,
|
||||
traitIndex: index,
|
||||
properties: {
|
||||
[fullKey]: ref.current?.value,
|
||||
},
|
||||
})
|
||||
: genOperation('modifyComponentProperty', {
|
||||
: genOperation(registry, 'modifyComponentProperty', {
|
||||
componentId: selectedComponentId,
|
||||
properties: {
|
||||
[fullKey]: ref.current?.value,
|
||||
@ -65,6 +66,7 @@ export const renderField = (properties: {
|
||||
type: type,
|
||||
fullKey: `${fullKey}.${childKey}`,
|
||||
selectedComponentId,
|
||||
services,
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -73,7 +75,8 @@ export const renderField = (properties: {
|
||||
};
|
||||
|
||||
export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
const { registry } = props;
|
||||
const { services } = props;
|
||||
const { editorStore, registry, eventBus } = services;
|
||||
const { selectedComponent, selectedComponentId } = editorStore;
|
||||
if (!selectedComponentId) {
|
||||
return <div>No components selected. Click on a component to select it.</div>;
|
||||
@ -92,7 +95,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
const changeComponentId = (selectedComponentId: string, value: string) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyComponentId', {
|
||||
genOperation(registry, 'modifyComponentId', {
|
||||
componentId: selectedComponentId,
|
||||
newId: value,
|
||||
})
|
||||
@ -136,7 +139,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
onChange={newFormData => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyComponentProperty', {
|
||||
genOperation(registry, 'modifyComponentProperty', {
|
||||
componentId: selectedComponentId,
|
||||
properties: newFormData,
|
||||
})
|
||||
@ -146,10 +149,10 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
<EventTraitForm component={selectedComponent} registry={registry} />
|
||||
<FetchTraitForm component={selectedComponent} registry={registry} />
|
||||
<StyleTraitForm component={selectedComponent} registry={registry} />
|
||||
<GeneralTraitFormList component={selectedComponent} registry={registry} />
|
||||
<EventTraitForm component={selectedComponent} services={services} />
|
||||
<FetchTraitForm component={selectedComponent} services={services} />
|
||||
<StyleTraitForm component={selectedComponent} services={services} />
|
||||
<GeneralTraitFormList component={selectedComponent} services={services} />
|
||||
</VStack>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
@ -13,31 +13,36 @@ import { Static } from '@sinclair/typebox';
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useFormik } from 'formik';
|
||||
import { EventHandlerSchema, Registry } from '@sunmao-ui/runtime';
|
||||
import { EventHandlerSchema } from '@sunmao-ui/runtime';
|
||||
import { formWrapperCSS } from '../style';
|
||||
import { KeyValueEditor } from '../../KeyValueEditor';
|
||||
import { editorStore } from '../../../EditorStore';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
eventTypes: readonly string[];
|
||||
handler: Static<typeof EventHandlerSchema>;
|
||||
onChange: (hanlder: Static<typeof EventHandlerSchema>) => void;
|
||||
onRemove: () => void;
|
||||
hideEventType?: boolean;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const EventHandlerForm: React.FC<Props> = observer(props => {
|
||||
const { handler, eventTypes, onChange, onRemove, hideEventType, registry } = props;
|
||||
const { handler, eventTypes, onChange, onRemove, hideEventType, services } = props;
|
||||
const { registry, editorStore } = services;
|
||||
const { components } = editorStore;
|
||||
const [methods, setMethods] = useState<string[]>([]);
|
||||
|
||||
const updateMethods = useCallback((componentId: string) => {
|
||||
const type = components.find(c => c.id === componentId)?.type;
|
||||
if (type) {
|
||||
const componentSpec = registry.getComponentByType(type).spec;
|
||||
setMethods(Object.keys(componentSpec.methods));
|
||||
}
|
||||
}, [components, registry])
|
||||
const updateMethods = useCallback(
|
||||
(componentId: string) => {
|
||||
const type = components.find(c => c.id === componentId)?.type;
|
||||
if (type) {
|
||||
const componentSpec = registry.getComponentByType(type).spec;
|
||||
setMethods(Object.keys(componentSpec.methods));
|
||||
}
|
||||
},
|
||||
[components, registry]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (handler.componentId) {
|
||||
|
@ -5,20 +5,20 @@ import { Static } from '@sinclair/typebox';
|
||||
import produce from 'immer';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { EventHandlerSchema } from '@sunmao-ui/runtime';
|
||||
import { eventBus } from '../../../eventBus';
|
||||
import { EventHandlerForm } from './EventHandlerForm';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { genOperation } from '../../../operations';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type EventHandler = Static<typeof EventHandlerSchema>;
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
component: ComponentSchema;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const EventTraitForm: React.FC<Props> = props => {
|
||||
const { component, registry } = props;
|
||||
const { component, services } = props;
|
||||
const { eventBus, registry } = services;
|
||||
|
||||
const handlers: EventHandler[] = useMemo(() => {
|
||||
return component.traits.find(t => t.type === 'core/v1/event')?.properties
|
||||
@ -48,7 +48,7 @@ export const EventTraitForm: React.FC<Props> = props => {
|
||||
if (!handlers) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createTrait', {
|
||||
genOperation(registry, 'createTrait', {
|
||||
componentId: component.id,
|
||||
traitType: 'core/v1/event',
|
||||
properties: { handlers: [newHandler] }
|
||||
@ -59,7 +59,7 @@ export const EventTraitForm: React.FC<Props> = props => {
|
||||
const index = component.traits.findIndex(t => t.type === 'core/v1/event');
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyTraitProperty', {
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: component.id,
|
||||
traitIndex: index,
|
||||
properties: { handlers: [...handlers, newHandler] }
|
||||
@ -77,7 +77,7 @@ export const EventTraitForm: React.FC<Props> = props => {
|
||||
});
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyTraitProperty', {
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: component.id,
|
||||
traitIndex: index,
|
||||
properties: {
|
||||
@ -94,7 +94,7 @@ export const EventTraitForm: React.FC<Props> = props => {
|
||||
});
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyTraitProperty', {
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: component.id,
|
||||
traitIndex: index,
|
||||
properties: {
|
||||
@ -110,7 +110,7 @@ export const EventTraitForm: React.FC<Props> = props => {
|
||||
key={i}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
registry={registry}
|
||||
services={services}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useFormik } from 'formik';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
@ -10,43 +12,40 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import { useFormik } from 'formik';
|
||||
import produce from 'immer';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { EventCallBackHandlerSchema, FetchTraitPropertiesSchema } from '@sunmao-ui/runtime';
|
||||
import {
|
||||
EventCallBackHandlerSchema,
|
||||
FetchTraitPropertiesSchema,
|
||||
} from '@sunmao-ui/runtime';
|
||||
import { formWrapperCSS } from '../style';
|
||||
import { KeyValueEditor } from '../../KeyValueEditor';
|
||||
import { EventHandlerForm } from '../EventTraitForm/EventHandlerForm';
|
||||
import { eventBus } from '../../../eventBus';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { genOperation } from '../../../operations';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type EventHandler = Static<typeof EventCallBackHandlerSchema>;
|
||||
|
||||
type Props = {
|
||||
component: ComponentSchema;
|
||||
registry: Registry;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
const httpMethods = ['get', 'post', 'put', 'delete', 'patch'];
|
||||
|
||||
export const FetchTraitForm: React.FC<Props> = props => {
|
||||
const { component, registry } = props;
|
||||
const { component, services } = props;
|
||||
const { registry, eventBus } = services;
|
||||
|
||||
const fetchTrait = component.traits.find(t => t.type === 'core/v1/fetch')
|
||||
?.properties as Static<typeof FetchTraitPropertiesSchema>;
|
||||
|
||||
if (!fetchTrait) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: fetchTrait,
|
||||
onSubmit: values => {
|
||||
const index = component.traits.findIndex(t => t.type === 'core/v1/fetch');
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyTraitProperty', {
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: component.id,
|
||||
traitIndex: index,
|
||||
properties: values,
|
||||
@ -55,6 +54,10 @@ export const FetchTraitForm: React.FC<Props> = props => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!fetchTrait) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlField = (
|
||||
<FormControl>
|
||||
<FormLabel>URL</FormLabel>
|
||||
@ -161,11 +164,11 @@ export const FetchTraitForm: React.FC<Props> = props => {
|
||||
<EventHandlerForm
|
||||
key={i}
|
||||
eventTypes={[]}
|
||||
handler={{type: '', ...handler}}
|
||||
handler={{ type: '', ...handler }}
|
||||
hideEventType={true}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
registry={registry}
|
||||
services={services}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -197,7 +200,7 @@ export const FetchTraitForm: React.FC<Props> = props => {
|
||||
const index = component.traits.findIndex(t => t.type === 'core/v1/fetch');
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeTrait', {
|
||||
genOperation(registry, 'removeTrait', {
|
||||
componentId: component.id,
|
||||
index,
|
||||
})
|
||||
|
@ -5,18 +5,19 @@ import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import { renderField } from '../ComponentForm';
|
||||
import { formWrapperCSS } from '../style';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
component: ComponentSchema;
|
||||
trait: TraitSchema;
|
||||
traitIndex: number;
|
||||
onRemove: () => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const GeneralTraitForm: React.FC<Props> = props => {
|
||||
const { trait, traitIndex, component, onRemove, registry } = props;
|
||||
const { trait, traitIndex, component, onRemove, services } = props;
|
||||
const { registry } = services;
|
||||
|
||||
const tImpl = registry.getTraitByType(trait.type);
|
||||
const properties = Object.assign(
|
||||
@ -32,6 +33,7 @@ export const GeneralTraitForm: React.FC<Props> = props => {
|
||||
fullKey: key,
|
||||
type: trait.type,
|
||||
selectedComponentId: component.id,
|
||||
services,
|
||||
});
|
||||
});
|
||||
return (
|
||||
|
@ -1,28 +1,29 @@
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { parseTypeBox } from '@sunmao-ui/runtime';
|
||||
import { HStack, VStack } from '@chakra-ui/react';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
|
||||
import { AddTraitButton } from './AddTraitButton';
|
||||
import { GeneralTraitForm } from './GeneralTraitForm';
|
||||
import { eventBus } from '../../../eventBus';
|
||||
import { ignoreTraitsList } from '../../../constants';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { genOperation } from '../../../operations';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
component: ComponentSchema;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const GeneralTraitFormList: React.FC<Props> = props => {
|
||||
const { component, registry } = props;
|
||||
const { component, services } = props;
|
||||
const { eventBus, registry } = services;
|
||||
|
||||
const onAddTrait = (type: string) => {
|
||||
const traitSpec = registry.getTraitByType(type).spec;
|
||||
const initProperties = parseTypeBox(traitSpec.properties as TSchema);
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createTrait', {
|
||||
genOperation(registry, 'createTrait', {
|
||||
componentId: component.id,
|
||||
traitType: type,
|
||||
properties: initProperties,
|
||||
@ -30,31 +31,30 @@ export const GeneralTraitFormList: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const traitFields = component.traits
|
||||
.map((trait, index) => {
|
||||
if (ignoreTraitsList.includes(trait.type)) {
|
||||
return null
|
||||
}
|
||||
const onRemoveTrait = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeTrait', {
|
||||
componentId: component.id,
|
||||
index,
|
||||
})
|
||||
);
|
||||
};
|
||||
return (
|
||||
<GeneralTraitForm
|
||||
key={index}
|
||||
component={component}
|
||||
trait={trait}
|
||||
traitIndex={index}
|
||||
onRemove={onRemoveTrait}
|
||||
registry={registry}
|
||||
/>
|
||||
const traitFields = component.traits.map((trait, index) => {
|
||||
if (ignoreTraitsList.includes(trait.type)) {
|
||||
return null;
|
||||
}
|
||||
const onRemoveTrait = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'removeTrait', {
|
||||
componentId: component.id,
|
||||
index,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<GeneralTraitForm
|
||||
key={index}
|
||||
component={component}
|
||||
trait={trait}
|
||||
traitIndex={index}
|
||||
onRemove={onRemoveTrait}
|
||||
services={services}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack width="full" alignItems="start">
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import produce from 'immer';
|
||||
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
@ -9,17 +11,14 @@ import {
|
||||
Select,
|
||||
} from '@chakra-ui/react';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import { CssEditor } from '../../../components/CodeEditor';
|
||||
import { eventBus } from '../../../eventBus';
|
||||
import { genOperation } from '../../../operations';
|
||||
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import produce from 'immer';
|
||||
import { formWrapperCSS } from '../style';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
component: ComponentSchema;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
type Styles = Array<{
|
||||
@ -28,7 +27,8 @@ type Styles = Array<{
|
||||
}>;
|
||||
|
||||
export const StyleTraitForm: React.FC<Props> = props => {
|
||||
const { component, registry } = props;
|
||||
const { component, services } = props;
|
||||
const { eventBus, registry } = services;
|
||||
|
||||
const styleSlots = useMemo(() => {
|
||||
return registry.getComponentByType(component.type).spec.styleSlots;
|
||||
@ -39,12 +39,12 @@ export const StyleTraitForm: React.FC<Props> = props => {
|
||||
}, [component]);
|
||||
|
||||
const styleTrait = component.traits[styleTraitIndex];
|
||||
const styles = (styleTrait?.properties.styles as Styles) || [];
|
||||
const ddddddd = styleTrait?.properties.styles as Styles | undefined;
|
||||
|
||||
const createStyleTrait = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createTrait', {
|
||||
genOperation(registry, 'createTrait', {
|
||||
componentId: component.id,
|
||||
traitType: 'core/v1/style',
|
||||
properties: {
|
||||
@ -63,7 +63,7 @@ export const StyleTraitForm: React.FC<Props> = props => {
|
||||
(newStyles: Styles) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyTraitProperty', {
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: component.id,
|
||||
traitIndex: styleTraitIndex,
|
||||
properties: {
|
||||
@ -72,16 +72,16 @@ export const StyleTraitForm: React.FC<Props> = props => {
|
||||
})
|
||||
);
|
||||
},
|
||||
[component, styleTraitIndex]
|
||||
[component.id, eventBus, registry, styleTraitIndex]
|
||||
);
|
||||
|
||||
const addStyle = useCallback(() => {
|
||||
const newStyles: Styles = styles.concat({
|
||||
const newStyles: Styles = (ddddddd || []).concat({
|
||||
styleSlot: styleSlots[0],
|
||||
style: '',
|
||||
});
|
||||
updateStyles(newStyles);
|
||||
}, [updateStyles, styleSlots, styles]);
|
||||
}, [updateStyles, styleSlots, ddddddd]);
|
||||
|
||||
const onClickCreate = () => {
|
||||
if (!styleTrait) {
|
||||
@ -93,67 +93,64 @@ export const StyleTraitForm: React.FC<Props> = props => {
|
||||
|
||||
const changeStyleContent = useCallback(
|
||||
(i: number, value: string) => {
|
||||
const newStyles = produce(styles, draft => {
|
||||
if (!ddddddd) return;
|
||||
const newStyles = produce(ddddddd, draft => {
|
||||
draft[i].style = value;
|
||||
});
|
||||
updateStyles(newStyles);
|
||||
},
|
||||
[updateStyles, styles]
|
||||
[updateStyles, ddddddd]
|
||||
);
|
||||
|
||||
const changeStyleSlot = useCallback(
|
||||
(i: number, newSlot: string) => {
|
||||
const newStyles = produce(styles, draft => {
|
||||
if (!ddddddd) return;
|
||||
const newStyles = produce(ddddddd, draft => {
|
||||
draft[i].styleSlot = newSlot;
|
||||
});
|
||||
updateStyles(newStyles);
|
||||
},
|
||||
[updateStyles, styles]
|
||||
[updateStyles, ddddddd]
|
||||
);
|
||||
|
||||
const styleForms = useMemo(
|
||||
() =>
|
||||
styles.map(({ style, styleSlot }, i) => {
|
||||
if (styles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const removeStyle = () => {
|
||||
const newStyles = styles.filter((_, j) => j !== i);
|
||||
updateStyles(newStyles);
|
||||
};
|
||||
return (
|
||||
<VStack key={`${styleSlot}-${i}`} css={formWrapperCSS} spacing="2">
|
||||
<FormControl id={styleSlot}>
|
||||
<FormLabel marginInlineEnd="0">
|
||||
<HStack width="full" justify="space-between">
|
||||
<Text>Style Slot</Text>
|
||||
<IconButton
|
||||
aria-label="remove style"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
icon={<CloseIcon />}
|
||||
onClick={removeStyle}
|
||||
/>
|
||||
</HStack>
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={styleSlot}
|
||||
onChange={e => changeStyleSlot(i, e.target.value)}
|
||||
>
|
||||
{styleSlots.map(s => (
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<CssEditor defaultCode={style} onBlur={v => changeStyleContent(i, v)} />
|
||||
</VStack>
|
||||
);
|
||||
}),
|
||||
[styles, changeStyleContent, changeStyleSlot, updateStyles]
|
||||
);
|
||||
const styleForms = useMemo(() => {
|
||||
if (!ddddddd) {
|
||||
return null;
|
||||
}
|
||||
return ddddddd.map(({ style, styleSlot }, i) => {
|
||||
const removeStyle = () => {
|
||||
const newStyles = ddddddd.filter((_, j) => j !== i);
|
||||
updateStyles(newStyles);
|
||||
};
|
||||
return (
|
||||
<VStack key={`${styleSlot}-${i}`} css={formWrapperCSS} spacing="2">
|
||||
<FormControl id={styleSlot}>
|
||||
<FormLabel marginInlineEnd="0">
|
||||
<HStack width="full" justify="space-between">
|
||||
<Text>Style Slot</Text>
|
||||
<IconButton
|
||||
aria-label="remove style"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
icon={<CloseIcon />}
|
||||
onClick={removeStyle}
|
||||
/>
|
||||
</HStack>
|
||||
</FormLabel>
|
||||
<Select value={styleSlot} onChange={e => changeStyleSlot(i, e.target.value)}>
|
||||
{styleSlots.map(s => (
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<CssEditor defaultCode={style} onBlur={v => changeStyleContent(i, v)} />
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
}, [ddddddd, styleSlots, updateStyles, changeStyleSlot, changeStyleContent]);
|
||||
|
||||
return (
|
||||
<VStack width="full">
|
||||
|
@ -1,12 +1,12 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ComponentWrapperType } from '@sunmao-ui/runtime';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { editorStore } from '../EditorStore';
|
||||
import { registry } from '../setup';
|
||||
import { eventBus } from '../eventBus';
|
||||
|
||||
import { genOperation } from '../operations';
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { EditorServices } from '../types';
|
||||
|
||||
type ComponentEditorState = 'drag' | 'select' | 'hover' | 'idle';
|
||||
|
||||
@ -113,163 +113,166 @@ const outlineMaskStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ComponentWrapper: ComponentWrapperType = observer(props => {
|
||||
const { component, parentType } = props;
|
||||
const {
|
||||
selectedComponentId,
|
||||
setSelectedComponentId,
|
||||
hoverComponentId,
|
||||
setHoverComponentId,
|
||||
dragOverComponentId,
|
||||
setDragOverComponentId,
|
||||
} = editorStore;
|
||||
export function useComponentWrapper(services: EditorServices): ComponentWrapperType {
|
||||
const { editorStore, registry, eventBus } = services;
|
||||
return observer(props => {
|
||||
const { component, parentType } = props;
|
||||
const {
|
||||
selectedComponentId,
|
||||
setSelectedComponentId,
|
||||
hoverComponentId,
|
||||
setHoverComponentId,
|
||||
dragOverComponentId,
|
||||
setDragOverComponentId,
|
||||
} = editorStore;
|
||||
|
||||
const [slots, isDroppable] = useMemo(() => {
|
||||
const slots = registry.getComponentByType(component.type).spec.slots;
|
||||
return [slots, slots.length > 0];
|
||||
}, [component.type]);
|
||||
const [slots, isDroppable] = useMemo(() => {
|
||||
const slots = registry.getComponentByType(component.type).spec.slots;
|
||||
return [slots, slots.length > 0];
|
||||
}, [component.type]);
|
||||
|
||||
const [currentSlot, setCurrentSlot] = useState<string>();
|
||||
const [currentSlot, setCurrentSlot] = useState<string>();
|
||||
|
||||
const componentEditorState: ComponentEditorState = useMemo(() => {
|
||||
if (dragOverComponentId === component.id) {
|
||||
return 'drag';
|
||||
} else if (selectedComponentId === component.id) {
|
||||
return 'select';
|
||||
} else if (hoverComponentId === component.id) {
|
||||
return 'hover';
|
||||
} else {
|
||||
return 'idle';
|
||||
}
|
||||
}, [dragOverComponentId, selectedComponentId, hoverComponentId, component.id]);
|
||||
const componentEditorState: ComponentEditorState = useMemo(() => {
|
||||
if (dragOverComponentId === component.id) {
|
||||
return 'drag';
|
||||
} else if (selectedComponentId === component.id) {
|
||||
return 'select';
|
||||
} else if (hoverComponentId === component.id) {
|
||||
return 'hover';
|
||||
} else {
|
||||
return 'idle';
|
||||
}
|
||||
}, [dragOverComponentId, selectedComponentId, hoverComponentId, component.id]);
|
||||
|
||||
const [inline, fullHeight, vertical] = useMemo(() => {
|
||||
return [
|
||||
inlineList.includes(component.type),
|
||||
fullHeightList.includes(parentType),
|
||||
verticalStackList.includes(component.type),
|
||||
];
|
||||
}, [component.type, parentType]);
|
||||
const [inline, fullHeight, vertical] = useMemo(() => {
|
||||
return [
|
||||
inlineList.includes(component.type),
|
||||
fullHeightList.includes(parentType),
|
||||
verticalStackList.includes(component.type),
|
||||
];
|
||||
}, [component.type, parentType]);
|
||||
|
||||
const onClickWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedComponentId(component.id);
|
||||
};
|
||||
const onMouseEnterWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setHoverComponentId(component.id);
|
||||
};
|
||||
const onMouseLeaveWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setHoverComponentId('');
|
||||
};
|
||||
const onClickWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedComponentId(component.id);
|
||||
};
|
||||
const onMouseEnterWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setHoverComponentId(component.id);
|
||||
};
|
||||
const onMouseLeaveWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setHoverComponentId('');
|
||||
};
|
||||
|
||||
const onDragEnter = (e: React.DragEvent<HTMLElement>) => {
|
||||
if (!isDroppable) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const enter = findRelatedWrapper(e.target as HTMLElement);
|
||||
if (!enter) {
|
||||
// if enter a non-wrapper element (seems won't happen)
|
||||
setDragOverComponentId(dragOverComponentId);
|
||||
setCurrentSlot(undefined);
|
||||
return;
|
||||
}
|
||||
if (!enter.droppable) {
|
||||
// if not droppable element
|
||||
const onDragEnter = (e: React.DragEvent<HTMLElement>) => {
|
||||
if (!isDroppable) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const enter = findRelatedWrapper(e.target as HTMLElement);
|
||||
if (!enter) {
|
||||
// if enter a non-wrapper element (seems won't happen)
|
||||
setDragOverComponentId(dragOverComponentId);
|
||||
setCurrentSlot(undefined);
|
||||
return;
|
||||
}
|
||||
if (!enter.droppable) {
|
||||
// if not droppable element
|
||||
setDragOverComponentId('');
|
||||
setCurrentSlot(undefined);
|
||||
return;
|
||||
}
|
||||
// update dragover component id
|
||||
if (dragOverComponentId !== enter.id) {
|
||||
setDragOverComponentId(enter.id);
|
||||
setCurrentSlot(enter.slot);
|
||||
} else if (currentSlot !== enter.slot && enter.slot) {
|
||||
setCurrentSlot(enter.slot);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = (e: React.DragEvent<HTMLElement>) => {
|
||||
// not processing leave event when no element is marked as dragover
|
||||
if (!isDroppable || !dragOverComponentId) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const enter = findRelatedWrapper(e.relatedTarget as HTMLElement);
|
||||
if (!enter) {
|
||||
// if entered element is not a sunmao wrapper, set dragId to ''
|
||||
setDragOverComponentId('');
|
||||
setCurrentSlot(undefined);
|
||||
} else if ((!enter.slot && !enter.droppable) || enter.id !== component.id) {
|
||||
setCurrentSlot(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (e: React.DragEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
if (!isDroppable) return;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = (e: React.DragEvent) => {
|
||||
if (!isDroppable) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragOverComponentId('');
|
||||
setCurrentSlot(undefined);
|
||||
return;
|
||||
}
|
||||
// update dragover component id
|
||||
if (dragOverComponentId !== enter.id) {
|
||||
setDragOverComponentId(enter.id);
|
||||
setCurrentSlot(enter.slot);
|
||||
} else if (currentSlot !== enter.slot && enter.slot) {
|
||||
setCurrentSlot(enter.slot);
|
||||
}
|
||||
};
|
||||
const creatingComponent = e.dataTransfer?.getData('component') || '';
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'createComponent', {
|
||||
componentType: creatingComponent,
|
||||
parentId: component.id,
|
||||
slot: currentSlot,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onDragLeave = (e: React.DragEvent<HTMLElement>) => {
|
||||
// not processing leave event when no element is marked as dragover
|
||||
if (!isDroppable || !dragOverComponentId) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const enter = findRelatedWrapper(e.relatedTarget as HTMLElement);
|
||||
if (!enter) {
|
||||
// if entered element is not a sunmao wrapper, set dragId to ''
|
||||
setDragOverComponentId('');
|
||||
setCurrentSlot(undefined);
|
||||
} else if ((!enter.slot && !enter.droppable) || enter.id !== component.id) {
|
||||
setCurrentSlot(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (e: React.DragEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
if (!isDroppable) return;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = (e: React.DragEvent) => {
|
||||
if (!isDroppable) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragOverComponentId('');
|
||||
setCurrentSlot(undefined);
|
||||
const creatingComponent = e.dataTransfer?.getData('component') || '';
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
componentType: creatingComponent,
|
||||
parentId: component.id,
|
||||
slot: currentSlot,
|
||||
})
|
||||
return (
|
||||
<div
|
||||
data-component={component.id}
|
||||
data-droppable={isDroppable}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onClick={onClickWrapper}
|
||||
onMouseEnter={onMouseEnterWrapper}
|
||||
onMouseLeave={onMouseLeaveWrapper}
|
||||
onDragOver={onDragOver}
|
||||
className={cx(
|
||||
ComponentWrapperStyle,
|
||||
inline ? 'inline' : undefined,
|
||||
fullHeight ? 'full-height' : undefined
|
||||
)}
|
||||
>
|
||||
<Text className={cx(outlineMaskTextStyle, componentEditorState)}>
|
||||
{component.id}
|
||||
</Text>
|
||||
{props.children}
|
||||
{isDroppable && componentEditorState === 'drag' ? (
|
||||
<div className={cx('slots-wrapper', vertical ? 'vertical' : undefined)}>
|
||||
{slots.map(slot => {
|
||||
return (
|
||||
<SlotWrapper
|
||||
componentId={component.id}
|
||||
state={slot === currentSlot ? 'over' : 'sibling'}
|
||||
key={slot}
|
||||
slotId={slot}
|
||||
></SlotWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cx(outlineMaskStyle, 'component', componentEditorState)}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component={component.id}
|
||||
data-droppable={isDroppable}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onClick={onClickWrapper}
|
||||
onMouseEnter={onMouseEnterWrapper}
|
||||
onMouseLeave={onMouseLeaveWrapper}
|
||||
onDragOver={onDragOver}
|
||||
className={cx(
|
||||
ComponentWrapperStyle,
|
||||
inline ? 'inline' : undefined,
|
||||
fullHeight ? 'full-height' : undefined
|
||||
)}
|
||||
>
|
||||
<Text className={cx(outlineMaskTextStyle, componentEditorState)}>
|
||||
{component.id}
|
||||
</Text>
|
||||
{props.children}
|
||||
{isDroppable && componentEditorState === 'drag' ? (
|
||||
<div className={cx('slots-wrapper', vertical ? 'vertical' : undefined)}>
|
||||
{slots.map(slot => {
|
||||
return (
|
||||
<SlotWrapper
|
||||
componentId={component.id}
|
||||
state={slot === currentSlot ? 'over' : 'sibling'}
|
||||
key={slot}
|
||||
slotId={slot}
|
||||
></SlotWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cx(outlineMaskStyle, 'component', componentEditorState)}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const SlotWrapperTyle = css`
|
||||
flex-grow: 1;
|
||||
|
@ -4,19 +4,18 @@ import { GridCallbacks, DIALOG_CONTAINER_ID, initSunmaoUI } from '@sunmao-ui/run
|
||||
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel, Flex } from '@chakra-ui/react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { StructureTree } from './StructureTree';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { ComponentList } from './ComponentsList';
|
||||
import { EditorHeader } from './EditorHeader';
|
||||
import { KeyboardEventWrapper } from './KeyboardEventWrapper';
|
||||
import { ComponentWrapper } from './ComponentWrapper';
|
||||
import { useComponentWrapper } from './ComponentWrapper';
|
||||
import { StateEditor, SchemaEditor } from './CodeEditor';
|
||||
import { Explorer } from './Explorer';
|
||||
import { editorStore } from '../EditorStore';
|
||||
import { genOperation } from '../operations';
|
||||
import { ComponentForm } from './ComponentForm';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import { PreviewModal } from './PreviewModal';
|
||||
import { WarningArea } from './WarningArea';
|
||||
import { EditorServices } from '../types';
|
||||
|
||||
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
|
||||
|
||||
@ -24,198 +23,220 @@ type Props = {
|
||||
App: ReturnOfInit['App'];
|
||||
registry: ReturnOfInit['registry'];
|
||||
stateStore: ReturnOfInit['stateManager']['store'];
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const Editor: React.FC<Props> = observer(({ App, registry, stateStore }) => {
|
||||
const { components, selectedComponentId, modules } = editorStore;
|
||||
export const Editor: React.FC<Props> = observer(
|
||||
({ App, registry, stateStore, services }) => {
|
||||
const { eventBus, editorStore } = services;
|
||||
const { components, selectedComponentId, modules } = editorStore;
|
||||
|
||||
const [scale, setScale] = useState(100);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const [codeMode, setCodeMode] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [scale, setScale] = useState(100);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const [codeMode, setCodeMode] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
|
||||
const gridCallbacks: GridCallbacks = useMemo(() => {
|
||||
return {
|
||||
// drag an existing component
|
||||
onDragStop(id, layout) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyComponentProperty', {
|
||||
componentId: id,
|
||||
properties: { layout },
|
||||
})
|
||||
);
|
||||
},
|
||||
// drag a new component from tool box
|
||||
onDrop(id, layout, _, e) {
|
||||
const component = e.dataTransfer?.getData('component') || '';
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
componentType: component,
|
||||
parentId: id,
|
||||
slot: 'content',
|
||||
layout,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
const gridCallbacks: GridCallbacks = useMemo(() => {
|
||||
return {
|
||||
// drag an existing component
|
||||
onDragStop(id, layout) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'modifyComponentProperty', {
|
||||
componentId: id,
|
||||
properties: { layout },
|
||||
})
|
||||
);
|
||||
},
|
||||
// drag a new component from tool box
|
||||
onDrop(id, layout, _, e) {
|
||||
const component = e.dataTransfer?.getData('component') || '';
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'createComponent', {
|
||||
componentType: component,
|
||||
parentId: id,
|
||||
slot: 'content',
|
||||
layout,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [eventBus, registry]);
|
||||
|
||||
const app = useMemo<Application>(() => {
|
||||
return {
|
||||
version: 'sunmao/v1',
|
||||
kind: 'Application',
|
||||
metadata: {
|
||||
name: 'some App',
|
||||
},
|
||||
spec: {
|
||||
components,
|
||||
},
|
||||
};
|
||||
}, [components]);
|
||||
const app = useMemo<Application>(() => {
|
||||
return {
|
||||
version: 'sunmao/v1',
|
||||
kind: 'Application',
|
||||
metadata: {
|
||||
name: 'some App',
|
||||
},
|
||||
spec: {
|
||||
components,
|
||||
},
|
||||
};
|
||||
}, [components]);
|
||||
|
||||
const appComponent = useMemo(() => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<App
|
||||
options={app}
|
||||
debugEvent={false}
|
||||
debugStore={false}
|
||||
gridCallbacks={gridCallbacks}
|
||||
componentWrapper={ComponentWrapper}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}, [App, app, gridCallbacks]);
|
||||
const ComponentWrapper = useMemo(() => {
|
||||
return useComponentWrapper(services);
|
||||
}, [services]);
|
||||
|
||||
const renderMain = () => {
|
||||
const appBox = (
|
||||
<Box flex="1" background="gray.50" p={1} overflow="hidden">
|
||||
<Box
|
||||
width="full"
|
||||
height="full"
|
||||
background="white"
|
||||
overflow="auto"
|
||||
transform={`scale(${scale / 100})`}
|
||||
position="relative"
|
||||
>
|
||||
<Box id={DIALOG_CONTAINER_ID} width="full" height="full" position="absolute" />
|
||||
<Box width="full" overflow="auto">
|
||||
{appComponent}
|
||||
</Box>
|
||||
<WarningArea />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (codeMode) {
|
||||
const appComponent = useMemo(() => {
|
||||
return (
|
||||
<Flex width="100%" height="100%">
|
||||
<Box flex="1">
|
||||
<SchemaEditor defaultCode={JSON.stringify(app, null, 2)} onChange={setCode} />
|
||||
<ErrorBoundary>
|
||||
<App
|
||||
options={app}
|
||||
debugEvent={false}
|
||||
debugStore={false}
|
||||
gridCallbacks={gridCallbacks}
|
||||
componentWrapper={ComponentWrapper}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}, [App, ComponentWrapper, app, gridCallbacks]);
|
||||
|
||||
const renderMain = () => {
|
||||
const appBox = (
|
||||
<Box flex="1" background="gray.50" p={1} overflow="hidden">
|
||||
<Box
|
||||
width="full"
|
||||
height="full"
|
||||
background="white"
|
||||
overflow="auto"
|
||||
transform={`scale(${scale / 100})`}
|
||||
position="relative"
|
||||
>
|
||||
<Box
|
||||
id={DIALOG_CONTAINER_ID}
|
||||
width="full"
|
||||
height="full"
|
||||
position="absolute"
|
||||
/>
|
||||
<Box width="full" overflow="auto">
|
||||
{appComponent}
|
||||
</Box>
|
||||
<WarningArea services={services} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (codeMode) {
|
||||
return (
|
||||
<Flex width="100%" height="100%">
|
||||
<Box flex="1">
|
||||
<SchemaEditor
|
||||
defaultCode={JSON.stringify(app, null, 2)}
|
||||
onChange={setCode}
|
||||
/>
|
||||
</Box>
|
||||
{appBox}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
width="280px"
|
||||
minWidth="280px"
|
||||
borderRightWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<Tabs
|
||||
align="center"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
textAlign="left"
|
||||
isLazy
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Explorer</Tab>
|
||||
<Tab>UI Tree</Tab>
|
||||
<Tab>State</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel>
|
||||
<Explorer services={services}/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<StructureTree
|
||||
components={components}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={id => editorStore.setSelectedComponentId(id)}
|
||||
services={services}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} height="100%">
|
||||
<StateEditor code={JSON.stringify(stateStore, null, 2)} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{appBox}
|
||||
</Flex>
|
||||
<Box
|
||||
width="320px"
|
||||
minWidth="320px"
|
||||
borderLeftWidth="1px"
|
||||
borderColor="gray.200"
|
||||
overflow="auto"
|
||||
>
|
||||
<Tabs
|
||||
align="center"
|
||||
textAlign="left"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Inspect</Tab>
|
||||
<Tab>Insert</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto" background="gray.50">
|
||||
<TabPanel p={0}>
|
||||
<ComponentForm services={services} />
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<ComponentList registry={registry} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box width="280px" minWidth="280px" borderRightWidth="1px" borderColor="gray.200">
|
||||
<Tabs
|
||||
align="center"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
textAlign="left"
|
||||
isLazy
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Explorer</Tab>
|
||||
<Tab>UI Tree</Tab>
|
||||
<Tab>State</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel>
|
||||
<Explorer />
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<StructureTree
|
||||
components={components}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={id => editorStore.setSelectedComponentId(id)}
|
||||
registry={registry}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} height="100%">
|
||||
<StateEditor code={JSON.stringify(stateStore, null, 2)} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{appBox}
|
||||
<Box
|
||||
width="320px"
|
||||
minWidth="320px"
|
||||
borderLeftWidth="1px"
|
||||
borderColor="gray.200"
|
||||
overflow="auto"
|
||||
>
|
||||
<Tabs
|
||||
align="center"
|
||||
textAlign="left"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Inspect</Tab>
|
||||
<Tab>Insert</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto" background="gray.50">
|
||||
<TabPanel p={0}>
|
||||
<ComponentForm registry={registry} />
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<ComponentList registry={registry} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardEventWrapper
|
||||
components={components}
|
||||
selectedComponentId={selectedComponentId}
|
||||
>
|
||||
<Box display="flex" height="100%" width="100%" flexDirection="column">
|
||||
<EditorHeader
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
onPreview={() => setPreview(true)}
|
||||
codeMode={codeMode}
|
||||
onCodeMode={v => {
|
||||
setCodeMode(v);
|
||||
if (!v && code) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('replaceApp', {
|
||||
app: JSON.parse(code).spec.components,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" flex="1" overflow="auto">
|
||||
{renderMain()}
|
||||
return (
|
||||
<KeyboardEventWrapper
|
||||
components={components}
|
||||
selectedComponentId={selectedComponentId}
|
||||
services={services}
|
||||
>
|
||||
<Box display="flex" height="100%" width="100%" flexDirection="column">
|
||||
<EditorHeader
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
onPreview={() => setPreview(true)}
|
||||
codeMode={codeMode}
|
||||
onCodeMode={v => {
|
||||
setCodeMode(v);
|
||||
if (!v && code) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'replaceApp', {
|
||||
app: JSON.parse(code).spec.components,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" flex="1" overflow="auto">
|
||||
{renderMain()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{preview && (
|
||||
<PreviewModal onClose={() => setPreview(false)} app={app} modules={modules} />
|
||||
)}
|
||||
</KeyboardEventWrapper>
|
||||
);
|
||||
});
|
||||
{preview && (
|
||||
<PreviewModal onClose={() => setPreview(false)} app={app} modules={modules} />
|
||||
)}
|
||||
</KeyboardEventWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -2,8 +2,13 @@ import React from 'react';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { ExplorerForm } from './ExplorerForm/ExplorerForm';
|
||||
import { ExplorerTree } from './ExplorerTree';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
export const Explorer: React.FC = () => {
|
||||
type Props = {
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const Explorer: React.FC<Props> = ({ services }) => {
|
||||
const [isEditingMode, setIsEditingMode] = React.useState(false);
|
||||
const [formType, setFormType] = React.useState<'app' | 'module'>('app');
|
||||
const [currentVersion, setCurrentVersion] = React.useState<string>('');
|
||||
@ -25,9 +30,14 @@ export const Explorer: React.FC = () => {
|
||||
version={currentVersion}
|
||||
name={currentName}
|
||||
onBack={onBack}
|
||||
services={services}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
return <ErrorBoundary><ExplorerTree onEdit={onEdit} /></ErrorBoundary>;
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ExplorerTree onEdit={onEdit} services={services} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { FormControl, FormLabel, Input, VStack } from '@chakra-ui/react';
|
||||
import { useFormik } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { editorStore } from '../../../EditorStore';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type AppMetaDataFormData = {
|
||||
name: string;
|
||||
@ -11,36 +11,40 @@ type AppMetaDataFormData = {
|
||||
|
||||
type AppMetaDataFormProps = {
|
||||
data: AppMetaDataFormData;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const AppMetaDataForm: React.FC<AppMetaDataFormProps> = observer(({ data }) => {
|
||||
const onSubmit = (value: AppMetaDataFormData) => {
|
||||
editorStore.appStorage.saveAppMetaDataInLS(value)
|
||||
};
|
||||
const formik = useFormik({
|
||||
initialValues: data,
|
||||
onSubmit,
|
||||
});
|
||||
return (
|
||||
<VStack>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>App Version</FormLabel>
|
||||
<Input
|
||||
name="version"
|
||||
value={formik.values.version}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => formik.submitForm()}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<Input
|
||||
name="name"
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => formik.submitForm()}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
export const AppMetaDataForm: React.FC<AppMetaDataFormProps> = observer(
|
||||
({ data, services }) => {
|
||||
const { editorStore } = services;
|
||||
const onSubmit = (value: AppMetaDataFormData) => {
|
||||
editorStore.appStorage.saveAppMetaDataInLS(value);
|
||||
};
|
||||
const formik = useFormik({
|
||||
initialValues: data,
|
||||
onSubmit,
|
||||
});
|
||||
return (
|
||||
<VStack>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>App Version</FormLabel>
|
||||
<Input
|
||||
name="version"
|
||||
value={formik.values.version}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => formik.submitForm()}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<Input
|
||||
name="name"
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => formik.submitForm()}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -4,17 +4,19 @@ import { Button, Text, VStack } from '@chakra-ui/react';
|
||||
import { ArrowLeftIcon } from '@chakra-ui/icons';
|
||||
import { AppMetaDataForm } from './AppMetaDataForm';
|
||||
import { ModuleMetaDataForm } from './ModuleMetaDataForm';
|
||||
import { editorStore } from '../../../EditorStore';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
formType: 'app' | 'module';
|
||||
version: string;
|
||||
name: string;
|
||||
onBack: () => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const ExplorerForm: React.FC<Props> = observer(
|
||||
({ formType, version, name, onBack }) => {
|
||||
({ formType, version, name, onBack, services }) => {
|
||||
const { editorStore } = services;
|
||||
let form;
|
||||
switch (formType) {
|
||||
case 'app':
|
||||
@ -22,7 +24,7 @@ export const ExplorerForm: React.FC<Props> = observer(
|
||||
name,
|
||||
version,
|
||||
};
|
||||
form = <AppMetaDataForm data={appMetaData} />;
|
||||
form = <AppMetaDataForm data={appMetaData} services={services} />;
|
||||
break;
|
||||
case 'module':
|
||||
const moduleSpec = editorStore.appStorage.modules.find(
|
||||
@ -33,7 +35,7 @@ export const ExplorerForm: React.FC<Props> = observer(
|
||||
version,
|
||||
stateMap: moduleSpec?.spec.stateMap || {},
|
||||
};
|
||||
form = <ModuleMetaDataForm initData={moduleMetaData} />;
|
||||
form = <ModuleMetaDataForm services={services} initData={moduleMetaData} />;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||
import { FormControl, FormLabel, Input, VStack } from '@chakra-ui/react';
|
||||
import { useFormik } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { editorStore } from '../../../EditorStore';
|
||||
import { KeyValueEditor } from '../../KeyValueEditor';
|
||||
import { EditorServices } from '../../../types';
|
||||
|
||||
type ModuleMetaDataFormData = {
|
||||
name: string;
|
||||
@ -13,10 +13,12 @@ type ModuleMetaDataFormData = {
|
||||
|
||||
type ModuleMetaDataFormProps = {
|
||||
initData: ModuleMetaDataFormData;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const ModuleMetaDataForm: React.FC<ModuleMetaDataFormProps> = observer(
|
||||
({ initData }) => {
|
||||
({ initData, services }) => {
|
||||
const { editorStore } = services;
|
||||
const onSubmit = (value: ModuleMetaDataFormData) => {
|
||||
editorStore.appStorage.saveModuleMetaDataInLS(
|
||||
{ originName: initData.name, originVersion: initData.version },
|
||||
|
@ -3,102 +3,107 @@ import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
|
||||
import { ImplementedRuntimeModule } from '@sunmao-ui/runtime';
|
||||
import { editorStore } from '../../EditorStore';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
type ExplorerTreeProps = {
|
||||
onEdit: (kind: 'app' | 'module', version: string, name: string) => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
function genItemId(kind: 'app' | 'module', version: string, name: string) {
|
||||
return `${kind}-${version}-${name}`;
|
||||
}
|
||||
|
||||
export const ExplorerTree: React.FC<ExplorerTreeProps> = observer(({ onEdit }) => {
|
||||
const { app, modules, currentEditingTarget, updateCurrentEditingTarget } = editorStore;
|
||||
const appItemId = genItemId('app', app.version, app.metadata.name);
|
||||
const [selectedItem, setSelectedItem] = React.useState<string | undefined>(
|
||||
genItemId(
|
||||
currentEditingTarget.kind,
|
||||
currentEditingTarget.version,
|
||||
currentEditingTarget.name
|
||||
)
|
||||
);
|
||||
export const ExplorerTree: React.FC<ExplorerTreeProps> = observer(
|
||||
({ onEdit, services }) => {
|
||||
const { editorStore } = services;
|
||||
const { app, modules, currentEditingTarget, updateCurrentEditingTarget } =
|
||||
editorStore;
|
||||
const appItemId = genItemId('app', app.version, app.metadata.name);
|
||||
const [selectedItem, setSelectedItem] = React.useState<string | undefined>(
|
||||
genItemId(
|
||||
currentEditingTarget.kind,
|
||||
currentEditingTarget.version,
|
||||
currentEditingTarget.name
|
||||
)
|
||||
);
|
||||
|
||||
const onClickApp = () => {
|
||||
setSelectedItem(appItemId);
|
||||
updateCurrentEditingTarget('app', app.version, app.metadata.name);
|
||||
};
|
||||
const onEditApp = () => {
|
||||
onEdit('app', app.version, app.metadata.name);
|
||||
};
|
||||
|
||||
const appEditable =
|
||||
currentEditingTarget.kind === 'app' &&
|
||||
currentEditingTarget.name === app.metadata.name &&
|
||||
currentEditingTarget.version === app.version;
|
||||
|
||||
const appItem = (
|
||||
<ExplorerTreeItem
|
||||
key={app.metadata.name}
|
||||
title={`${app.version}/${app.metadata.name}`}
|
||||
onClick={onClickApp}
|
||||
isActive={selectedItem === appItemId}
|
||||
onEdit={onEditApp}
|
||||
editable={appEditable}
|
||||
/>
|
||||
);
|
||||
|
||||
const moduleItems = modules.map((module: ImplementedRuntimeModule) => {
|
||||
const moduleItemId = genItemId('module', module.version, module.metadata.name);
|
||||
const onClickModule = () => {
|
||||
setSelectedItem(moduleItemId);
|
||||
updateCurrentEditingTarget('module', module.version, module.metadata.name);
|
||||
const onClickApp = () => {
|
||||
setSelectedItem(appItemId);
|
||||
updateCurrentEditingTarget('app', app.version, app.metadata.name);
|
||||
};
|
||||
const onEditModule = () => {
|
||||
onEdit('module', module.version, module.metadata.name);
|
||||
const onEditApp = () => {
|
||||
onEdit('app', app.version, app.metadata.name);
|
||||
};
|
||||
const onRemove = () => {
|
||||
editorStore.appStorage.removeModule(module.version, module.metadata.name);
|
||||
};
|
||||
const editable =
|
||||
currentEditingTarget.kind === 'module' &&
|
||||
currentEditingTarget.name === module.metadata.name &&
|
||||
currentEditingTarget.version === module.version;
|
||||
return (
|
||||
|
||||
const appEditable =
|
||||
currentEditingTarget.kind === 'app' &&
|
||||
currentEditingTarget.name === app.metadata.name &&
|
||||
currentEditingTarget.version === app.version;
|
||||
|
||||
const appItem = (
|
||||
<ExplorerTreeItem
|
||||
key={module.metadata.name}
|
||||
title={`${module.version}/${module.metadata.name}`}
|
||||
onClick={onClickModule}
|
||||
onRemove={onRemove}
|
||||
isActive={selectedItem === moduleItemId}
|
||||
onEdit={onEditModule}
|
||||
editable={editable}
|
||||
key={app.metadata.name}
|
||||
title={`${app.version}/${app.metadata.name}`}
|
||||
onClick={onClickApp}
|
||||
isActive={selectedItem === appItemId}
|
||||
onEdit={onEditApp}
|
||||
editable={appEditable}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack alignItems="start">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Applications
|
||||
</Text>
|
||||
{appItem}
|
||||
<Divider />
|
||||
<HStack width="full" justifyContent="space-between">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Modules
|
||||
</Text>
|
||||
<IconButton
|
||||
aria-label="create module"
|
||||
size="xs"
|
||||
icon={<AddIcon />}
|
||||
onClick={() => editorStore.appStorage.createModule()}
|
||||
const moduleItems = modules.map((module: ImplementedRuntimeModule) => {
|
||||
const moduleItemId = genItemId('module', module.version, module.metadata.name);
|
||||
const onClickModule = () => {
|
||||
setSelectedItem(moduleItemId);
|
||||
updateCurrentEditingTarget('module', module.version, module.metadata.name);
|
||||
};
|
||||
const onEditModule = () => {
|
||||
onEdit('module', module.version, module.metadata.name);
|
||||
};
|
||||
const onRemove = () => {
|
||||
editorStore.appStorage.removeModule(module.version, module.metadata.name);
|
||||
};
|
||||
const editable =
|
||||
currentEditingTarget.kind === 'module' &&
|
||||
currentEditingTarget.name === module.metadata.name &&
|
||||
currentEditingTarget.version === module.version;
|
||||
return (
|
||||
<ExplorerTreeItem
|
||||
key={module.metadata.name}
|
||||
title={`${module.version}/${module.metadata.name}`}
|
||||
onClick={onClickModule}
|
||||
onRemove={onRemove}
|
||||
isActive={selectedItem === moduleItemId}
|
||||
onEdit={onEditModule}
|
||||
editable={editable}
|
||||
/>
|
||||
</HStack>
|
||||
{moduleItems}
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack alignItems="start">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Applications
|
||||
</Text>
|
||||
{appItem}
|
||||
<Divider />
|
||||
<HStack width="full" justifyContent="space-between">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Modules
|
||||
</Text>
|
||||
<IconButton
|
||||
aria-label="create module"
|
||||
size="xs"
|
||||
icon={<AddIcon />}
|
||||
onClick={() => editorStore.appStorage.createModule()}
|
||||
/>
|
||||
</HStack>
|
||||
{moduleItems}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type ExplorerTreeItemProps = {
|
||||
title: string;
|
||||
|
@ -1,21 +1,24 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { css } from '@emotion/css';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import React, { useRef } from 'react';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { genOperation } from '../operations';
|
||||
import { PasteManager } from '../operations/PasteManager';
|
||||
import { EditorServices } from '../types';
|
||||
|
||||
type Props = {
|
||||
selectedComponentId: string;
|
||||
components: ComponentSchema[];
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const KeyboardEventWrapper: React.FC<Props> = ({
|
||||
selectedComponentId,
|
||||
components,
|
||||
services,
|
||||
children,
|
||||
}) => {
|
||||
const { eventBus,registry } = services;
|
||||
const pasteManager = useRef(new PasteManager());
|
||||
const style = css`
|
||||
&:focus {
|
||||
@ -29,7 +32,7 @@ export const KeyboardEventWrapper: React.FC<Props> = ({
|
||||
case 'Backspace':
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeComponent', {
|
||||
genOperation(registry, 'removeComponent', {
|
||||
componentId: selectedComponentId,
|
||||
})
|
||||
);
|
||||
@ -60,7 +63,7 @@ export const KeyboardEventWrapper: React.FC<Props> = ({
|
||||
pasteManager.current.setPasteComponents(selectedComponentId, components);
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeComponent', {
|
||||
genOperation(registry, 'removeComponent', {
|
||||
componentId: selectedComponentId,
|
||||
})
|
||||
);
|
||||
@ -70,7 +73,7 @@ export const KeyboardEventWrapper: React.FC<Props> = ({
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('pasteComponent', {
|
||||
genOperation(registry, 'pasteComponent', {
|
||||
parentId: selectedComponentId,
|
||||
slot: 'content',
|
||||
rootComponentId: pasteManager.current.rootComponentId,
|
||||
|
@ -1,24 +1,24 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Text, VStack } from '@chakra-ui/react';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { eventBus } from '../../eventBus';
|
||||
import { ComponentItemView } from './ComponentItemView';
|
||||
import { DropComponentWrapper } from './DropComponentWrapper';
|
||||
import { ChildrenMap } from './StructureTree';
|
||||
import { genOperation } from '../../operations';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
component: ComponentSchema;
|
||||
childrenMap: ChildrenMap;
|
||||
selectedComponentId: string;
|
||||
onSelectComponent: (id: string) => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const ComponentTree: React.FC<Props> = props => {
|
||||
const { component, childrenMap, selectedComponentId, onSelectComponent, registry } =
|
||||
const { component, childrenMap, selectedComponentId, onSelectComponent, services } =
|
||||
props;
|
||||
const { registry, eventBus } = services;
|
||||
const slots = registry.getComponentByType(component.type).spec.slots;
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
@ -39,7 +39,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
childrenMap={childrenMap}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={onSelectComponent}
|
||||
registry={registry}
|
||||
services={services}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -53,7 +53,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
const onCreateComponent = (creatingComponent: string) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
genOperation(registry, 'createComponent', {
|
||||
componentType: creatingComponent,
|
||||
parentId: component.id,
|
||||
slot,
|
||||
@ -65,7 +65,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
if (movingComponent === component.id) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
genOperation(registry, 'moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: component.id,
|
||||
slot,
|
||||
@ -74,7 +74,10 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
const slotName = (
|
||||
<DropComponentWrapper onCreateComponent={onCreateComponent} onMoveComponent={onMoveComponent}>
|
||||
<DropComponentWrapper
|
||||
onCreateComponent={onCreateComponent}
|
||||
onMoveComponent={onMoveComponent}
|
||||
>
|
||||
<Text color="gray.500" fontWeight="medium">
|
||||
Slot: {slot}
|
||||
</Text>
|
||||
@ -92,12 +95,12 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
}, [slots, childrenMap, component.id, selectedComponentId, onSelectComponent, registry]);
|
||||
}, [slots, childrenMap, component.id, selectedComponentId, onSelectComponent, services, eventBus, registry]);
|
||||
|
||||
const onClickRemove = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeComponent', {
|
||||
genOperation(registry, 'removeComponent', {
|
||||
componentId: component.id,
|
||||
})
|
||||
);
|
||||
@ -107,7 +110,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
if (slots.length === 0) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
genOperation(registry, 'createComponent', {
|
||||
componentType: creatingComponent,
|
||||
parentId: component.id,
|
||||
slot: slots[0],
|
||||
@ -118,7 +121,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
const onMoveUp = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('adjustComponentOrder', {
|
||||
genOperation(registry, 'adjustComponentOrder', {
|
||||
componentId: component.id,
|
||||
orientation: 'up',
|
||||
})
|
||||
@ -128,7 +131,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
const onMoveDown = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('adjustComponentOrder', {
|
||||
genOperation(registry, 'adjustComponentOrder', {
|
||||
componentId: component.id,
|
||||
orientation: 'down',
|
||||
})
|
||||
@ -139,7 +142,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
if (movingComponent === component.id) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
genOperation(registry, 'moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: component.id,
|
||||
slot: slots[0],
|
||||
@ -154,7 +157,10 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
width="full"
|
||||
alignItems="start"
|
||||
>
|
||||
<DropComponentWrapper onCreateComponent={onCreateComponent} onMoveComponent={onMoveComponent}>
|
||||
<DropComponentWrapper
|
||||
onCreateComponent={onCreateComponent}
|
||||
onMoveComponent={onMoveComponent}
|
||||
>
|
||||
<ComponentItemView
|
||||
id={component.id}
|
||||
title={component.id}
|
||||
|
@ -1,27 +1,27 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { Box, Text, VStack } from '@chakra-ui/react';
|
||||
import { eventBus } from '../../eventBus';
|
||||
import { ComponentItemView } from './ComponentItemView';
|
||||
import { ComponentTree } from './ComponentTree';
|
||||
import { DropComponentWrapper } from './DropComponentWrapper';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { genOperation as genOperation } from '../../operations';
|
||||
import { resolveApplicationComponents } from '../../utils/resolveApplicationComponents';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
export type ChildrenMap = Map<string, SlotsMap>;
|
||||
type SlotsMap = Map<string, ComponentSchema[]>;
|
||||
|
||||
type Props = {
|
||||
registry: Registry;
|
||||
components: ComponentSchema[];
|
||||
selectedComponentId: string;
|
||||
onSelectComponent: (id: string) => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const StructureTree: React.FC<Props> = props => {
|
||||
const { components, selectedComponentId, onSelectComponent, registry } = props;
|
||||
const { components, selectedComponentId, onSelectComponent, services } = props;
|
||||
const { eventBus, registry } = services;
|
||||
|
||||
const [realComponents, dataSources] = useMemo(() => {
|
||||
const _realComponent: ComponentSchema[] = [];
|
||||
@ -37,7 +37,8 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
}, [components]);
|
||||
|
||||
const componentEles = useMemo(() => {
|
||||
const { topLevelComponents, childrenMap } = resolveApplicationComponents(realComponents);
|
||||
const { topLevelComponents, childrenMap } =
|
||||
resolveApplicationComponents(realComponents);
|
||||
|
||||
return topLevelComponents.map(c => (
|
||||
<ComponentTree
|
||||
@ -46,17 +47,17 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
childrenMap={childrenMap}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={onSelectComponent}
|
||||
registry={registry}
|
||||
services={services}
|
||||
/>
|
||||
));
|
||||
}, [realComponents, selectedComponentId, onSelectComponent, registry]);
|
||||
}, [realComponents, selectedComponentId, onSelectComponent, services]);
|
||||
|
||||
const dataSourceEles = useMemo(() => {
|
||||
return dataSources.map(dummy => {
|
||||
const onClickRemove = () => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeComponent', {
|
||||
genOperation(registry, 'removeComponent', {
|
||||
componentId: dummy.id,
|
||||
})
|
||||
);
|
||||
@ -75,14 +76,14 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [dataSources, selectedComponentId, onSelectComponent]);
|
||||
}, [dataSources, selectedComponentId, eventBus, registry, onSelectComponent]);
|
||||
|
||||
return (
|
||||
<VStack spacing="2" padding="5" alignItems="start">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Components
|
||||
</Text>
|
||||
<RootItem />
|
||||
<RootItem services={services} />
|
||||
{componentEles}
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
DataSources
|
||||
@ -92,11 +93,12 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
function RootItem() {
|
||||
function RootItem(props: { services: EditorServices }) {
|
||||
const { eventBus, registry } = props.services;
|
||||
const onCreateComponent = (creatingComponent: string) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
genOperation(registry, 'createComponent', {
|
||||
componentType: creatingComponent,
|
||||
})
|
||||
);
|
||||
@ -105,7 +107,7 @@ function RootItem() {
|
||||
if (movingComponent === 'root') return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
genOperation(registry, 'moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: '__root__',
|
||||
slot: '__root__',
|
||||
|
@ -15,9 +15,14 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useMemo } from 'react';
|
||||
import { editorStore } from '../EditorStore';
|
||||
import { EditorServices } from '../types';
|
||||
|
||||
export const WarningArea: React.FC = observer(() => {
|
||||
type Props = {
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const WarningArea: React.FC<Props> = observer(({ services }) => {
|
||||
const { editorStore } = services;
|
||||
const [isCollapsed, setIsCollapsed] = React.useState(true);
|
||||
const errorItems = useMemo(() => {
|
||||
if (isCollapsed) {
|
||||
@ -39,7 +44,7 @@ export const WarningArea: React.FC = observer(() => {
|
||||
</Tr>
|
||||
);
|
||||
});
|
||||
}, [isCollapsed, editorStore.validateResult]);
|
||||
}, [isCollapsed, editorStore]);
|
||||
|
||||
const savedBadge = useMemo(() => {
|
||||
return <Badge colorScheme="green">Saved</Badge>;
|
||||
@ -59,7 +64,7 @@ export const WarningArea: React.FC = observer(() => {
|
||||
<Badge colorScheme="red">Unsave</Badge>
|
||||
</HStack>
|
||||
);
|
||||
}, []);
|
||||
}, [editorStore]);
|
||||
|
||||
return (
|
||||
<VStack
|
||||
|
@ -14,10 +14,14 @@ export type EventNames = {
|
||||
selectComponent: string;
|
||||
};
|
||||
|
||||
const emitter = mitt<EventNames>();
|
||||
|
||||
export const eventBus = {
|
||||
on: emitter.on,
|
||||
off: emitter.off,
|
||||
send: emitter.emit,
|
||||
};
|
||||
export const initEventBus = () => {
|
||||
const emitter = mitt<EventNames>();
|
||||
return {
|
||||
on: emitter.on,
|
||||
off: emitter.off,
|
||||
send: emitter.emit,
|
||||
};
|
||||
}
|
||||
|
||||
export type EventBusType = ReturnType<typeof initEventBus>
|
||||
|
1
packages/editor/src/index.ts
Normal file
1
packages/editor/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { initSunmaoEditor } from './init';
|
71
packages/editor/src/init.tsx
Normal file
71
packages/editor/src/init.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Editor as _Editor } from './components/Editor';
|
||||
import { initSunmaoUI } from '@sunmao-ui/runtime';
|
||||
import { AppModelManager } from './operations/AppModelManager';
|
||||
import React from 'react';
|
||||
import {
|
||||
ChakraProvider,
|
||||
extendTheme,
|
||||
withDefaultSize,
|
||||
withDefaultVariant,
|
||||
} from '@chakra-ui/react';
|
||||
import { initEventBus } from './eventBus';
|
||||
import { EditorStore } from './EditorStore';
|
||||
|
||||
export function initSunmaoEditor() {
|
||||
const editorTheme = extendTheme(
|
||||
withDefaultSize({
|
||||
size: 'sm',
|
||||
components: [
|
||||
'Input',
|
||||
'NumberInput',
|
||||
'Checkbox',
|
||||
'Radio',
|
||||
'Textarea',
|
||||
'Select',
|
||||
'Switch',
|
||||
],
|
||||
}),
|
||||
withDefaultVariant({
|
||||
variant: 'filled',
|
||||
components: ['Input', 'NumberInput', 'Textarea', 'Select'],
|
||||
})
|
||||
);
|
||||
|
||||
const ui = initSunmaoUI();
|
||||
|
||||
const App = ui.App;
|
||||
const registry = ui.registry;
|
||||
const stateManager = ui.stateManager;
|
||||
const eventBus = initEventBus();
|
||||
const appModelManager = new AppModelManager(eventBus);
|
||||
const editorStore = new EditorStore(eventBus, registry, stateManager);
|
||||
const services = {
|
||||
App,
|
||||
registry: ui.registry,
|
||||
apiService: ui.apiService,
|
||||
stateManager,
|
||||
appModelManager,
|
||||
eventBus,
|
||||
editorStore,
|
||||
};
|
||||
|
||||
const Editor: React.FC = () => {
|
||||
return (
|
||||
<ChakraProvider theme={editorTheme}>
|
||||
<_Editor
|
||||
App={App}
|
||||
registry={registry}
|
||||
stateStore={stateManager.store}
|
||||
services={services}
|
||||
/>
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
Editor,
|
||||
registry,
|
||||
onChange: () => null,
|
||||
onSave: () => null,
|
||||
};
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
import {
|
||||
ChakraProvider,
|
||||
extendTheme,
|
||||
withDefaultSize,
|
||||
withDefaultVariant,
|
||||
} from '@chakra-ui/react';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import { initSunmaoEditor } from './init';
|
||||
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
import { Editor } from './components/Editor';
|
||||
import { App as _App, registry, stateManager, ui } from './setup';
|
||||
|
||||
type Options = Partial<{
|
||||
components: Parameters<Registry['registerComponent']>[0][];
|
||||
@ -19,33 +14,7 @@ type Options = Partial<{
|
||||
container: Element;
|
||||
}>;
|
||||
|
||||
const editorTheme = extendTheme(
|
||||
withDefaultSize({
|
||||
size: 'sm',
|
||||
components: [
|
||||
'Input',
|
||||
'NumberInput',
|
||||
'Checkbox',
|
||||
'Radio',
|
||||
'Textarea',
|
||||
'Select',
|
||||
'Switch',
|
||||
],
|
||||
}),
|
||||
withDefaultVariant({
|
||||
variant: 'filled',
|
||||
components: ['Input', 'NumberInput', 'Textarea', 'Select'],
|
||||
})
|
||||
);
|
||||
|
||||
export const App: React.FC = () => {
|
||||
return (
|
||||
<ChakraProvider theme={editorTheme}>
|
||||
<Editor App={_App} registry={registry} stateStore={stateManager.store} />
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
export { registry, ui };
|
||||
const { Editor, registry } = initSunmaoEditor();
|
||||
|
||||
export default function renderApp(options: Options = {}) {
|
||||
const {
|
||||
@ -57,10 +26,11 @@ export default function renderApp(options: Options = {}) {
|
||||
components.forEach(c => registry.registerComponent(c));
|
||||
traits.forEach(t => registry.registerTrait(t));
|
||||
modules.forEach(m => registry.registerModule(m));
|
||||
registry.installLib(sunmaoChakraUILib);
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Editor />
|
||||
</StrictMode>,
|
||||
container
|
||||
);
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { EventBusType } from '../eventBus';
|
||||
import { IUndoRedoManager, IOperation, OperationList } from './type';
|
||||
|
||||
export class AppModelManager implements IUndoRedoManager {
|
||||
components: ComponentSchema[] = [];
|
||||
operationStack: OperationList<IOperation> = new OperationList();
|
||||
|
||||
constructor() {
|
||||
constructor(private eventBus: EventBusType) {
|
||||
eventBus.on('undo', () => this.undo());
|
||||
eventBus.on('redo', () => this.redo());
|
||||
eventBus.on('operation', o => this.do(o));
|
||||
@ -18,7 +18,7 @@ export class AppModelManager implements IUndoRedoManager {
|
||||
|
||||
updateComponents(components: ComponentSchema[]) {
|
||||
this.components = components;
|
||||
eventBus.send('componentsChange', this.components);
|
||||
this.eventBus.send('componentsChange', this.components);
|
||||
}
|
||||
|
||||
do(operation: IOperation): void {
|
||||
|
@ -24,7 +24,7 @@ export class CreateComponentBranchOperation extends BaseBranchOperation<CreateCo
|
||||
}
|
||||
// insert a new component to schema
|
||||
this.operationStack.insert(
|
||||
new CreateComponentLeafOperation({
|
||||
new CreateComponentLeafOperation(this.registry, {
|
||||
componentId: this.context.componentId!,
|
||||
componentType: this.context.componentType,
|
||||
parentId: this.context.parentId as ComponentId,
|
||||
@ -43,7 +43,7 @@ export class CreateComponentBranchOperation extends BaseBranchOperation<CreateCo
|
||||
} else if (parentComponent.type === 'core/v1/grid_layout') {
|
||||
this.operationStack.insert(
|
||||
// update grid layout for the new created component, it was pushed into layout by react-grid-layout, so we need to find it and update its id
|
||||
new ModifyComponentPropertiesLeafOperation({
|
||||
new ModifyComponentPropertiesLeafOperation(this.registry, {
|
||||
componentId: parentComponent.id,
|
||||
properties: {
|
||||
layout: (prev: Array<ReactGridLayout.Layout>) => {
|
||||
|
@ -12,11 +12,11 @@ export type ModifyComponentIdBranchOperationContext = {
|
||||
|
||||
export class ModifyComponentIdBranchOperation extends BaseBranchOperation<ModifyComponentIdBranchOperationContext> {
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
this.operationStack.insert(new ModifyComponentIdLeafOperation(this.context));
|
||||
this.operationStack.insert(new ModifyComponentIdLeafOperation(this.registry, this.context));
|
||||
|
||||
// update selectid
|
||||
this.operationStack.insert(
|
||||
new UpdateSelectComponentLeafOperation({
|
||||
new UpdateSelectComponentLeafOperation(this.registry, {
|
||||
// TODO: need a way to get selectedComponent.id here
|
||||
// componentId: ApplicationComponent[]Instance.selectedComponent?.id,
|
||||
componentId: '',
|
||||
|
@ -17,7 +17,7 @@ export class MoveComponentBranchOperation extends BaseBranchOperation<MoveCompon
|
||||
|
||||
if (this.context.toId === '__root__') {
|
||||
this.operationStack.insert(
|
||||
new RemoveTraitLeafOperation({
|
||||
new RemoveTraitLeafOperation(this.registry, {
|
||||
componentId: this.context.fromId,
|
||||
index: traitIndex,
|
||||
})
|
||||
@ -28,7 +28,7 @@ export class MoveComponentBranchOperation extends BaseBranchOperation<MoveCompon
|
||||
};
|
||||
if (traitIndex > -1) {
|
||||
this.operationStack.insert(
|
||||
new ModifyTraitPropertiesLeafOperation({
|
||||
new ModifyTraitPropertiesLeafOperation(this.registry, {
|
||||
componentId: this.context.fromId,
|
||||
traitIndex,
|
||||
properties: newSlotProperties,
|
||||
@ -36,7 +36,7 @@ export class MoveComponentBranchOperation extends BaseBranchOperation<MoveCompon
|
||||
);
|
||||
} else {
|
||||
this.operationStack.insert(
|
||||
new CreateTraitLeafOperation({
|
||||
new CreateTraitLeafOperation(this.registry, {
|
||||
componentId: this.context.fromId,
|
||||
traitType: 'core/v1/slot',
|
||||
properties: newSlotProperties,
|
||||
|
@ -14,13 +14,13 @@ export type RemoveComponentBranchOperationContext = {
|
||||
|
||||
export class RemoveComponentBranchOperation extends BaseBranchOperation<RemoveComponentBranchOperationContext> {
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const parent = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
|
||||
if (parent && parent.type === 'core/v1/grid_layout') {
|
||||
// modify layout property of parent grid layout component
|
||||
this.operationStack.insert(
|
||||
new ModifyComponentPropertiesLeafOperation({
|
||||
new ModifyComponentPropertiesLeafOperation(this.registry, {
|
||||
componentId: parent.id,
|
||||
properties: {
|
||||
layout: (prev: Array<ReactGridLayout.Layout>) => {
|
||||
@ -41,7 +41,7 @@ export class RemoveComponentBranchOperation extends BaseBranchOperation<RemoveCo
|
||||
|
||||
// free component from schema
|
||||
this.operationStack.insert(
|
||||
new RemoveComponentLeafOperation({ componentId: this.context.componentId })
|
||||
new RemoveComponentLeafOperation(this.registry, { componentId: this.context.componentId })
|
||||
);
|
||||
|
||||
// do the operation in order
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import {
|
||||
CreateComponentBranchOperation,
|
||||
CreateComponentBranchOperationContext,
|
||||
@ -46,8 +47,9 @@ const OperationConstructors: Record<
|
||||
type OperationTypes = keyof OperationConfigMaps;
|
||||
|
||||
type OperationConfigMap<TOperation, TContext> = {
|
||||
constructor: new (context: TContext) => TOperation;
|
||||
constructor: new (registry: Registry, context: TContext) => TOperation;
|
||||
context: TContext;
|
||||
registry: Registry;
|
||||
};
|
||||
|
||||
type OperationConfigMaps = {
|
||||
@ -96,11 +98,12 @@ type OperationConfigMaps = {
|
||||
};
|
||||
|
||||
export const genOperation = <T extends OperationTypes>(
|
||||
registry: Registry,
|
||||
type: T,
|
||||
context: OperationConfigMaps[T]['context']
|
||||
context: OperationConfigMaps[T]['context'],
|
||||
): IOperation => {
|
||||
const OperationConstructor = OperationConstructors[
|
||||
type
|
||||
] as OperationConfigMaps[T]['constructor'];
|
||||
return new OperationConstructor(context as any);
|
||||
return new OperationConstructor(registry, context as any);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export class AdjustComponentOrderLeafOperation extends BaseLeafOperation<AdjustC
|
||||
}
|
||||
|
||||
private move(prev: ComponentSchema[], orientation: 'up' | 'down'): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
|
@ -14,7 +14,7 @@ export class CreateComponentLeafOperation extends BaseLeafOperation<CreateCompon
|
||||
private component!: IComponentModel;
|
||||
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.createComponent(this.context.componentType as ComponentType, this.context.componentId as ComponentId);
|
||||
if (this.context.parentId) {
|
||||
const parent = appModel.getComponentById(this.context.parentId);
|
||||
@ -34,7 +34,7 @@ export class CreateComponentLeafOperation extends BaseLeafOperation<CreateCompon
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
appModel.removeComponent(this.component.id)
|
||||
return appModel.toSchema()
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export type ModifyComponentIdLeafOperationContext = {
|
||||
|
||||
export class ModifyComponentIdLeafOperation extends BaseLeafOperation<ModifyComponentIdLeafOperationContext> {
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
@ -23,7 +23,7 @@ export class ModifyComponentIdLeafOperation extends BaseLeafOperation<ModifyComp
|
||||
return this.do(prev);
|
||||
}
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.newId as ComponentId)!;
|
||||
component.changeId(this.context.componentId as ComponentId);
|
||||
return appModel.toSchema();
|
||||
|
@ -11,7 +11,7 @@ export type ModifyComponentPropertiesLeafOperationContext = {
|
||||
export class ModifyComponentPropertiesLeafOperation extends BaseLeafOperation<ModifyComponentPropertiesLeafOperationContext> {
|
||||
private previousState: Record<string, any> = {};
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (component) {
|
||||
for (const property in this.context.properties) {
|
||||
@ -35,7 +35,7 @@ export class ModifyComponentPropertiesLeafOperation extends BaseLeafOperation<Mo
|
||||
return newSchema;
|
||||
}
|
||||
redo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
@ -48,7 +48,7 @@ export class ModifyComponentPropertiesLeafOperation extends BaseLeafOperation<Mo
|
||||
return appModel.toSchema();
|
||||
}
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
|
@ -15,12 +15,12 @@ export class PasteComponentLeafOperation extends BaseLeafOperation<PasteComponen
|
||||
private componentCopy!: IComponentModel
|
||||
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const targetParent = appModel.getComponentById(this.context.parentId as ComponentId);
|
||||
if (!targetParent) {
|
||||
return prev
|
||||
}
|
||||
const copyComponents = new AppModel(this.context.components);
|
||||
const copyComponents = new AppModel(this.context.components, this.registry);
|
||||
const component = copyComponents.getComponentById(this.context.rootComponentId as ComponentId);
|
||||
if (!component){
|
||||
return prev;
|
||||
@ -39,7 +39,7 @@ export class PasteComponentLeafOperation extends BaseLeafOperation<PasteComponen
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
appModel.removeComponent(this.componentCopy.id);
|
||||
return appModel.toSchema()
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export class RemoveComponentLeafOperation extends BaseLeafOperation<RemoveCompon
|
||||
private prevComponent?: IComponentModel;
|
||||
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
this.deletedComponent = appModel.getComponentById(
|
||||
this.context.componentId as ComponentId
|
||||
);
|
||||
@ -29,7 +29,7 @@ export class RemoveComponentLeafOperation extends BaseLeafOperation<RemoveCompon
|
||||
if (!this.deletedComponent) {
|
||||
return prev;
|
||||
}
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const parent = appModel.getComponentById(
|
||||
this.deletedComponent.parentId as ComponentId
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
|
||||
private traitId!: TraitId;
|
||||
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
return prev
|
||||
@ -28,7 +28,7 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
return prev
|
||||
|
@ -13,7 +13,7 @@ export type ModifyTraitPropertiesLeafOperationContext = {
|
||||
export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation<ModifyTraitPropertiesLeafOperationContext> {
|
||||
private previousState: Record<string, any> = {};
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
@ -35,7 +35,7 @@ export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation<Modify
|
||||
return appModel.toSchema();
|
||||
}
|
||||
redo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
@ -51,7 +51,7 @@ export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation<Modify
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
|
@ -11,7 +11,7 @@ export type RemoveTraitLeafOperationContext = {
|
||||
export class RemoveTraitLeafOperation extends BaseLeafOperation<RemoveTraitLeafOperationContext> {
|
||||
private deletedTrait!: ITraitModel;
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
@ -28,7 +28,7 @@ export class RemoveTraitLeafOperation extends BaseLeafOperation<RemoveTraitLeafO
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
const appModel = new AppModel(prev);
|
||||
const appModel = new AppModel(prev, this.registry);
|
||||
const component = appModel.getComponentById(this.context.componentId as ComponentId);
|
||||
if (!component) {
|
||||
console.warn('component not found');
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { eventBus } from '../../eventBus';
|
||||
import { BaseLeafOperation } from '../type';
|
||||
|
||||
export type UpdateSelectComponentLeafOperationContext = {
|
||||
@ -8,25 +7,25 @@ export type UpdateSelectComponentLeafOperationContext = {
|
||||
};
|
||||
|
||||
export class UpdateSelectComponentLeafOperation extends BaseLeafOperation<UpdateSelectComponentLeafOperationContext> {
|
||||
private prevId!: string;
|
||||
prevId!: string;
|
||||
do(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
this.prevId = this.context.componentId || prev[0].id;
|
||||
setTimeout(() => {
|
||||
eventBus.send('selectComponent', this.context.newId);
|
||||
// eventBus.send('selectComponent', this.context.newId);
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
|
||||
redo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
setTimeout(() => {
|
||||
eventBus.send('selectComponent', this.context.newId);
|
||||
// eventBus.send('selectComponent', this.context.newId);
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
|
||||
undo(prev: ComponentSchema[]): ComponentSchema[] {
|
||||
setTimeout(() => {
|
||||
eventBus.send('selectComponent', this.prevId);
|
||||
// eventBus.send('selectComponent', this.prevId);
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ComponentSchema } from '@sunmao-ui/core';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
|
||||
export const leafSymbol = Symbol('leaf');
|
||||
export const branchSymbol = Symbol('branch');
|
||||
@ -140,9 +141,11 @@ export interface IOperation<TContext = any> extends IUndoRedo {
|
||||
*/
|
||||
export abstract class BaseLeafOperation<TContext> implements IOperation<TContext> {
|
||||
context: TContext;
|
||||
registry: Registry;
|
||||
type = leafSymbol;
|
||||
|
||||
constructor(context: TContext) {
|
||||
constructor(registry: Registry, context: TContext) {
|
||||
this.registry = registry;
|
||||
this.context = context;
|
||||
}
|
||||
/**
|
||||
@ -178,10 +181,12 @@ export abstract class BaseBranchOperation<TContext>
|
||||
{
|
||||
operationStack: OperationList;
|
||||
context: TContext;
|
||||
registry: Registry;
|
||||
type = branchSymbol;
|
||||
|
||||
constructor(context: TContext) {
|
||||
constructor(registry: Registry, context: TContext) {
|
||||
this.context = context;
|
||||
this.registry = registry;
|
||||
this.operationStack = new OperationList();
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ComponentSchema, parseType } from '@sunmao-ui/core';
|
||||
import { Registry } from '@sunmao-ui/runtime';
|
||||
import { isDraft, original } from 'immer';
|
||||
import { get } from 'lodash-es';
|
||||
import { registry } from '../setup';
|
||||
|
||||
export function genComponent(type: string, id: string): ComponentSchema {
|
||||
export function genComponent(registry: Registry, type: string, id: string): ComponentSchema {
|
||||
const { version, name } = parseType(type);
|
||||
const cImpl = registry.getComponent(version, name);
|
||||
const initProperties = cImpl.metadata.exampleProperties;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Flex, Box, ChakraProvider, Button } from '@chakra-ui/react';
|
||||
import { Application } from '@sunmao-ui/core';
|
||||
import { initSunmaoUI } from '@sunmao-ui/runtime';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { StrictMode, useMemo, useState } from 'react';
|
||||
import { StrictMode, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './styles.css';
|
||||
|
||||
import { Editor } from './components/Editor';
|
||||
import { initSunmaoEditor } from './init';
|
||||
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
|
||||
|
||||
type Example = {
|
||||
name: string;
|
||||
@ -16,33 +16,13 @@ type Example = {
|
||||
};
|
||||
};
|
||||
|
||||
const { Editor, registry } = initSunmaoEditor();
|
||||
|
||||
registry.installLib(sunmaoChakraUILib);
|
||||
|
||||
const Playground: React.FC<{ examples: Example[] }> = ({ examples }) => {
|
||||
const [example, setExample] = useState<Example | null>(examples[0]);
|
||||
|
||||
const { App, registry, stateStore } = useMemo(() => {
|
||||
if (!example) {
|
||||
return {};
|
||||
}
|
||||
const ui = initSunmaoUI();
|
||||
const App = ui.App;
|
||||
const registry = ui.registry;
|
||||
const apiService = ui.apiService;
|
||||
const stateStore = ui.stateManager.store;
|
||||
|
||||
const { modules = [] } = example.value;
|
||||
modules.forEach(m => {
|
||||
registry.registerModule(m);
|
||||
});
|
||||
localStorage.removeItem('schema');
|
||||
|
||||
return {
|
||||
App,
|
||||
registry,
|
||||
stateStore,
|
||||
apiService,
|
||||
};
|
||||
}, [example]);
|
||||
|
||||
return (
|
||||
<Flex width="100vw" height="100vh">
|
||||
<Box shadow="md">
|
||||
@ -77,12 +57,7 @@ const Playground: React.FC<{ examples: Example[] }> = ({ examples }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex="1">
|
||||
<Editor
|
||||
key={example!.name}
|
||||
App={App!}
|
||||
registry={registry!}
|
||||
stateStore={stateStore!}
|
||||
/>
|
||||
<Editor />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { initSunmaoUI } from '@sunmao-ui/runtime';
|
||||
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
|
||||
import { AppModelManager } from './operations/AppModelManager';
|
||||
|
||||
const ui = initSunmaoUI();
|
||||
|
||||
const App = ui.App;
|
||||
const registry = ui.registry;
|
||||
const apiService = ui.apiService;
|
||||
const stateManager = ui.stateManager;
|
||||
const appModelManager = new AppModelManager();
|
||||
registry.installLib(sunmaoChakraUILib);
|
||||
|
||||
export {
|
||||
ui,
|
||||
App,
|
||||
registry,
|
||||
apiService,
|
||||
stateManager,
|
||||
appModelManager,
|
||||
};
|
16
packages/editor/src/types.ts
Normal file
16
packages/editor/src/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { initSunmaoUI, Registry, StateManager } from '@sunmao-ui/runtime';
|
||||
import { EditorStore } from './EditorStore';
|
||||
import { EventBusType } from './eventBus';
|
||||
import { AppModelManager } from './operations/AppModelManager';
|
||||
|
||||
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
|
||||
|
||||
export type EditorServices = {
|
||||
App: ReturnOfInit['App'];
|
||||
registry: Registry;
|
||||
apiService: ReturnOfInit['apiService'];
|
||||
stateManager: StateManager;
|
||||
appModelManager: AppModelManager;
|
||||
eventBus: EventBusType;
|
||||
editorStore: EditorStore;
|
||||
};
|
@ -52,7 +52,7 @@ export class SchemaValidator implements ISchemaValidator {
|
||||
}
|
||||
|
||||
validate(components: ComponentSchema[]) {
|
||||
const appModel = new AppModel(components);
|
||||
const appModel = new AppModel(components, this.registry);
|
||||
this.genComponentIdSpecMap(components);
|
||||
this.result = [];
|
||||
const baseContext = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user