feat(extract): support generate stateMap

This commit is contained in:
Bowen Tan 2023-01-04 18:05:47 +08:00
parent b7f46361e1
commit 3d2a090530
10 changed files with 257 additions and 45 deletions

View File

@ -1,7 +1,7 @@
type OperationType =
| 'createComponent'
| 'removeComponent'
| 'modifyComponentProperty'
| 'modifyComponentProperties'
| 'modifyComponentId'
| 'adjustComponentOrder'
| 'createTrait'

View File

@ -100,7 +100,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
onChange={newFormData => {
eventBus.send(
'operation',
genOperation(registry, 'modifyComponentProperty', {
genOperation(registry, 'modifyComponentProperties', {
componentId: selectedComponentId,
properties: newFormData,
})

View File

@ -27,19 +27,24 @@ import {
import { Static } from '@sinclair/typebox';
import { uniq } from 'lodash';
import { RelationshipModal } from '../RelationshipModal';
import { OutsideExpRelation } from './type';
import { RenameStateForm } from './RenameStateForm';
import { OutsideExpRelationWithState } from '.';
type Props = {
componentId: string;
services: EditorServices;
};
type ExpressionRelation = {
// 里面exp依赖外面
type InsideExpRelation = {
componentId: string;
exp: string;
key: string;
};
export type MethodRelation = {
// 里面的event调用外面的method
export type InsideMethodRelation = {
handler: Static<typeof EventHandlerSpec>;
source: string;
target: string;
@ -48,8 +53,9 @@ export type MethodRelation = {
};
type AllRelations = {
exp: ExpressionRelation[];
method: MethodRelation[];
exp: InsideExpRelation[];
method: InsideMethodRelation[];
outsideExpRelations: OutsideExpRelation[];
};
export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) => {
@ -57,25 +63,32 @@ export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) =>
const { appModel } = appModelManager;
const [showRelationId, setShowRelationId] = useState('');
const radioMapRef = useRef<RefTreatmentMap>({});
const outsideExpRelationsValueRef = useRef<OutsideExpRelationWithState[]>([]);
const [moduleName, setModuleName] = useState('myModule0');
const [moduleVersion, setModuleVersion] = useState('custom/v1');
const { expressionRelations, methodRelations } = useMemo(() => {
const { expressionRelations, methodRelations, outsideExpRelations } = useMemo(() => {
const root = appModel.getComponentById(componentId as ComponentId)!;
const components = root.allComponents;
const allRelations = components.reduce(
const moduleComponents = root.allComponents;
const allRelations = moduleComponents.reduce(
(res, c) => {
const { expressionRelations, methodRelations } = getRelations(c, components);
const { expressionRelations, methodRelations } = getRelations(
c,
moduleComponents
);
res.exp = res.exp.concat(expressionRelations);
res.method = res.method.concat(methodRelations);
return res;
},
{ exp: [], method: [] } as AllRelations
{ exp: [], method: [], outsideExpRelations: [] } as AllRelations
);
const outsideExpRelations = getOutsideExpRelations(appModel.allComponents, root);
console.log('outsideExpRelations', outsideExpRelations);
console.log('allRelations', allRelations);
return {
expressionRelations: allRelations.exp,
methodRelations: allRelations.method,
outsideExpRelations,
};
}, [appModel, componentId]);
@ -111,26 +124,22 @@ export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) =>
}}
onClickComponent={id => setShowRelationId(id)}
/>
// <Table size="sm" border="1px solid" borderColor="gray.100">
// <Thead>
// <Tr>
// <Th>ComponentId</Th>
// <Th>Move In to</Th>
// <Th>Expression</Th>
// </Tr>
// </Thead>
// <Tbody>
// {uniqExpRelations.map((d, i) => {
// return (
// <Tr key={i}>
// <Td>{idLink(d)}</Td>
// {/* <Td>{d.key}</Td>
// <Td>{d.exp}</Td> */}
// </Tr>
// );
// })}
// </Tbody>
// </Table>
);
};
const renameStateForm = () => {
if (!outsideExpRelations.length) {
return <Placeholder />;
}
return (
<RenameStateForm
outsideExpRelations={outsideExpRelations}
onChange={v => {
outsideExpRelationsValueRef.current = v;
console.log('outsideExpRelationsValueRef', v);
}}
/>
);
};
@ -185,6 +194,7 @@ export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) =>
moduleName,
moduleVersion,
methodRelations,
outsideExpRelations: outsideExpRelationsValueRef.current,
});
};
@ -207,6 +217,10 @@ export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) =>
</Heading>
{expressionTable()}
</VStack>
<VStack width="full" alignItems="start">
<Heading size="md">State map Config</Heading>
{renameStateForm()}
</VStack>
<VStack width="full" alignItems="start">
<Heading size="md">Who calls my methods?</Heading>
{methodTable()}
@ -217,9 +231,44 @@ export const ExtractModuleView: React.FC<Props> = ({ componentId, services }) =>
);
};
// 获取外部组件中依赖Module内组件的表达式;
// TODO: 这里只检查了component的property没检查trait里面的
function getOutsideExpRelations(
allComponents: IComponentModel[],
moduleRoot: IComponentModel
): OutsideExpRelation[] {
const res: OutsideExpRelation[] = [];
const clonedRoot = moduleRoot.clone();
const ids = clonedRoot.allComponents.map(c => c.id);
allComponents.forEach(c => {
if (clonedRoot.appModel.getComponentById(c.id)) {
// 是module内的无视
return;
}
c.properties.traverse((field, key) => {
if (field.isDynamic) {
const relyRefs = Object.keys(field.refComponentInfos).filter(refId =>
ids.includes(refId as ComponentId)
);
relyRefs.forEach(refId => {
res.push({
componentId: c.id,
exp: field.getValue() as string,
key,
valuePath:
field.refComponentInfos[refId as ComponentId].refProperties.slice(-1)[0],
relyOn: refId,
});
});
}
});
});
return res;
}
function getRelations(component: IComponentModel, components: IComponentModel[]) {
const expressionRelations: ExpressionRelation[] = [];
const methodRelations: MethodRelation[] = [];
const expressionRelations: InsideExpRelation[] = [];
const methodRelations: InsideMethodRelation[] = [];
const ids = components.map(c => c.id) as string[];
// 获取到Module中用到的外部id
component.properties.traverse((field, key) => {
@ -273,6 +322,7 @@ function getRelations(component: IComponentModel, components: IComponentModel[])
});
}
});
return { expressionRelations, methodRelations };
}

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import { Input, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { OutsideExpRelation, OutsideExpRelationWithState } from './type';
import produce from 'immer';
type Props = {
outsideExpRelations: OutsideExpRelation[];
onChange: (value: OutsideExpRelationWithState[]) => void;
};
export const RenameStateForm: React.FC<Props> = ({ outsideExpRelations, onChange }) => {
const [value, setValue] = useState<OutsideExpRelationWithState[]>([]);
useEffect(() => {
const newValue = outsideExpRelations.map(r => {
return {
...r,
stateName: '',
};
});
setValue(newValue);
}, [outsideExpRelations]);
useEffect(() => {
onChange(value);
}, [onChange, value]);
return (
<Table size="sm" border="1px solid" borderColor="gray.100">
<Thead>
<Tr>
<Th>Comoonent</Th>
<Th>key</Th>
<Th>Expression</Th>
<Th>RawExp</Th>
<Th>StateName</Th>
</Tr>
</Thead>
<Tbody>
{value.map((d, i) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = produce(value, draft => {
draft[i].stateName = e.target.value;
return draft;
});
setValue(newValue);
};
return (
<Tr key={i}>
<Td>{d.componentId}</Td>
<Td>{d.key}</Td>
<Td>{d.exp}</Td>
<Td>{`${d.relyOn}.${d.valuePath}`}</Td>
<Td>
<Input size="sm" value={d.stateName} onChange={onChange} />
</Td>
</Tr>
);
})}
</Tbody>
</Table>
);
};

View File

@ -1 +1,2 @@
export * from './ExtractModuleModal';
export * from './type';

View File

@ -0,0 +1,12 @@
// 外面exp依赖里面
export type OutsideExpRelation = {
componentId: string;
exp: string;
key: string;
valuePath: string;
relyOn: string;
};
export type OutsideExpRelationWithState = OutsideExpRelation & {
stateName: string;
};

View File

@ -35,7 +35,7 @@ export const OperationConstructors: Record<
> = {
createComponent: CreateComponentBranchOperation,
removeComponent: RemoveComponentBranchOperation,
modifyComponentProperty: ModifyComponentPropertiesLeafOperation,
modifyComponentProperties: ModifyComponentPropertiesLeafOperation,
modifyComponentId: ModifyComponentIdBranchOperation,
adjustComponentOrder: AdjustComponentOrderLeafOperation,
createTrait: CreateTraitLeafOperation,
@ -64,7 +64,7 @@ export type OperationConfigMaps = {
RemoveComponentBranchOperation,
RemoveComponentBranchOperationContext
>;
modifyComponentProperty: OperationConfigMap<
modifyComponentProperties: OperationConfigMap<
ModifyComponentPropertiesLeafOperation,
ModifyComponentPropertiesLeafOperationContext
>;

View File

@ -0,0 +1,53 @@
import { BaseLeafOperation } from '../../type';
import { AppModel } from '../../../AppModel/AppModel';
import { ComponentId } from '../../../AppModel/IAppModel';
export type ModifyComponentPropertyLeafOperationContext = {
componentId: string;
path: string;
property: any;
};
export class ModifyComponentPropertyLeafOperation extends BaseLeafOperation<ModifyComponentPropertyLeafOperationContext> {
private previousValue: any = undefined;
do(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (component) {
const oldValue = component.properties.getPropertyByPath(this.context.path);
if (oldValue) {
// assign previous data
this.previousValue = oldValue;
const newValue = this.context.property;
oldValue.update(newValue);
} else {
console.warn('property not found');
}
} else {
console.warn('component not found');
}
return prev;
}
redo(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
console.warn('component not found');
return prev;
}
const newValue = this.context.property;
component.properties.getPropertyByPath(this.context.path)!.update(newValue);
return prev;
}
undo(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
console.warn('component not found');
return prev;
}
component.properties.getPropertyByPath(this.context.path)!.update(this.previousValue);
return prev;
}
}

View File

@ -44,7 +44,8 @@ export class AppStorage {
propertySpec?: JSONSchema7,
events?: string[],
moduleVersion?: string,
moduleName?: string
moduleName?: string,
stateMap?: Record<string, string>
): Module {
let index = this.modules.length;
@ -77,6 +78,9 @@ export class AppStorage {
if (events) {
newModule.spec.events = events;
}
if (stateMap) {
newModule.spec.stateMap = stateMap;
}
this.setModules([...this.modules, newModule]);
const rawModules = this.modules.map(addModuleId);

View File

@ -8,13 +8,14 @@ import { AppStorage } from './AppStorage';
import type { SchemaValidator, ValidateErrorResult } from '../validator';
import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
import { isEqual } from 'lodash';
import { isEqual, set } from 'lodash';
import { AppModelManager } from '../operations/AppModelManager';
import type { Metadata } from '@sunmao-ui/core';
import { ComponentId } from '../AppModel/IAppModel';
import { genOperation } from '../operations';
import { MethodRelation } from '../components/ExtractModuleModal/ExtractModuleView';
import { InsideMethodRelation } from '../components/ExtractModuleModal/ExtractModuleView';
import { Static } from '@sinclair/typebox';
import { OutsideExpRelationWithState } from '../components/ExtractModuleModal';
type EditingTarget = {
kind: 'app' | 'module';
@ -250,7 +251,8 @@ export class EditorStore {
toMoveComponentIds: string[];
moduleName: string;
moduleVersion: string;
methodRelations: MethodRelation[];
methodRelations: InsideMethodRelation[];
outsideExpRelations: OutsideExpRelationWithState[];
}) {
const {
id,
@ -259,12 +261,15 @@ export class EditorStore {
moduleName,
moduleVersion,
methodRelations,
outsideExpRelations,
} = props;
const root = this.appModelManager.appModel
.getComponentById(id as ComponentId)!
.clone();
console.log('toMoveComponentIds', toMoveComponentIds);
console.log('properties', properties);
const newModuleContainerId = `${id}__module`;
const newModuleId = `${id}Module`;
const propertySpec: Record<string, any> = {
type: 'object',
properties: { ...properties },
@ -335,6 +340,31 @@ export class EditorStore {
};
});
// 开始处理 State
const stateMap: Record<string, string> = {};
outsideExpRelations.forEach(r => {
// 添加 StateMap
if (r.stateName) {
const origin = `${r.relyOn}.${r.valuePath}`;
stateMap[r.stateName] = origin;
// 然后一个个替换Exp里的字符串
const newExp = r.exp.replaceAll(origin, `${newModuleId}.${r.stateName}`);
const c = this.appModelManager.appModel.getComponentById(
r.componentId as ComponentId
)!;
const fieldKey = r.key.startsWith('.') ? r.key.slice(1) : r.key;
const newProperties = set(c.properties.rawValue, fieldKey, newExp);
console.log('newProperties', newProperties);
this.eventBus.send(
'operation',
genOperation(this.registry, 'modifyComponentProperties', {
componentId: r.componentId,
properties: newProperties,
})
);
}
});
console.log('propertySpec', propertySpec);
console.log('moduleComponents', moduleComponents);
const rawModule = this.appStorage.createModule(
@ -342,26 +372,26 @@ export class EditorStore {
propertySpec,
eventSpec,
moduleVersion,
moduleName
moduleName,
stateMap
);
const module = createModule(rawModule);
this.registry.registerModule(module);
const newId = `${id}__module`;
this.eventBus.send(
'operation',
genOperation(this.registry, 'createComponent', {
componentId: newId,
componentId: newModuleContainerId,
componentType: `core/v1/moduleContainer`,
})
);
this.eventBus.send(
'operation',
genOperation(this.registry, 'modifyComponentProperty', {
componentId: newId,
genOperation(this.registry, 'modifyComponentProperties', {
componentId: newModuleContainerId,
properties: {
id: `${id}Module`,
id: newModuleId,
type: `${moduleVersion}/${moduleName}`,
properties,
handlers: moduleHandlers,