Merge pull request #705 from smartxworks/feat/module-event-method

Support module method
This commit is contained in:
tanbowensg 2023-03-30 16:05:41 +08:00 committed by GitHub
commit 38835afd81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 161 additions and 32 deletions

View File

@ -13,10 +13,17 @@ export type Module = {
impl: ComponentSchema[];
};
export type ModuleMethodSpec = {
name: string;
componentId: string;
componentMethod: string;
};
type ModuleSpec = {
properties: JSONSchema7;
events: string[];
stateMap: Record<string, string>;
methods: ModuleMethodSpec[];
};
// extended runtime
@ -46,6 +53,7 @@ export function createModule(options: CreateModuleOptions): RuntimeModule {
properties: { type: 'object' },
events: [],
stateMap: {},
methods: [],
...options.spec,
},
impl: options.impl || [],

View File

@ -3,7 +3,7 @@ import { FormControl, FormLabel, Input, Select } from '@chakra-ui/react';
import { Type, Static } from '@sinclair/typebox';
import { useFormik } from 'formik';
import { GLOBAL_UTIL_METHOD_ID } from '@sunmao-ui/runtime';
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentSchema, MethodSchema } from '@sunmao-ui/core';
import { WidgetProps } from '../../types/widget';
import { implementWidget, mergeWidgetOptionsIntoSpec } from '../../utils/widget';
import { RecordWidget } from './RecordField';
@ -16,6 +16,7 @@ import {
MountEvents,
GLOBAL_MODULE_ID,
ModuleEventMethodSpec,
isModuleContainer,
} from '@sunmao-ui/shared';
import { JSONSchema7Object } from 'json-schema';
import { PREVENT_POPOVER_WIDGET_CLOSE_CLASS } from '../../constants/widget';
@ -45,22 +46,47 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
},
});
const findMethodsByComponent = useCallback(
(component?: ComponentSchema) => {
if (!component) {
(targetComponent?: ComponentSchema) => {
if (!targetComponent) {
return [];
}
const componentMethods = Object.entries(
registry.getComponentByType(component.type).spec.methods
const componentMethods: MethodSchema[] = Object.entries(
registry.getComponentByType(targetComponent.type).spec.methods
).map(([name, parameters]) => ({
name,
parameters,
}));
const traitMethods = component.traits
const traitMethods: MethodSchema[] = targetComponent.traits
.map(trait => registry.getTraitByType(trait.type).spec.methods)
.flat();
return ([] as any[]).concat(componentMethods, traitMethods);
const moduleMethods: MethodSchema[] = [];
if (isModuleContainer(targetComponent)) {
const moduleType = targetComponent.properties.type as string;
const moduleSpec = registry.getModuleByType(moduleType);
moduleSpec.spec.methods.forEach(m => {
const innerComponent = moduleSpec.impl.find(c => c.id === m.componentId);
if (innerComponent) {
// find the method spec from the component or component's traits
const cMethod = registry.getComponentByType(innerComponent.type).spec.methods[
m.componentMethod
];
const tMethod = innerComponent.traits
.map(trait => registry.getTraitByType(trait.type).spec.methods)
.flat()
.find(_m => _m.name === m.componentMethod);
if (cMethod || tMethod) {
moduleMethods.push({
name: m.name,
parameters: cMethod || tMethod?.parameters,
});
}
}
});
}
return ([] as any[]).concat(componentMethods, traitMethods, moduleMethods);
},
[registry]
);
@ -97,7 +123,9 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
} else if (componentId === GLOBAL_MODULE_ID) {
spec = ModuleEventMethodSpec;
} else {
const targetComponent = appModelManager.appModel.getComponentById(componentId);
const targetComponent = appModelManager.appModel
.getComponentById(componentId)
.toSchema();
const targetMethod = (findMethodsByComponent(targetComponent) ?? []).find(
({ name }) => name === formik.values.method.name
);

View File

@ -6,6 +6,7 @@ import { ModuleMetaDataForm, ModuleMetaDataFormData } from './ModuleMetaDataForm
import { EditorServices } from '../../../types';
import { Type } from '@sinclair/typebox';
import { cloneDeep } from 'lodash';
import { json2JsonSchema } from '@sunmao-ui/editor-sdk';
type Props = {
formType: 'app' | 'module';
@ -30,9 +31,12 @@ export const ExplorerForm: React.FC<Props> = observer(
};
const saveModuleMetaData = () => {
if (!newModuleMetaDataRef.current) return;
const propertiesSpec = json2JsonSchema(
services.stateManager.deepEval(newModuleMetaDataRef.current.exampleProperties)
);
editorStore.appStorage.saveModuleMetaData(
{ originName: name, originVersion: version },
newModuleMetaDataRef.current
{ ...newModuleMetaDataRef.current, properties: propertiesSpec }
);
editorStore.setModuleDependencies(newModuleMetaDataRef.current.exampleProperties);
setCurrentVersion?.(newModuleMetaDataRef.current.version);
@ -85,6 +89,7 @@ export const ExplorerForm: React.FC<Props> = observer(
properties: moduleSpec?.spec.properties || Type.Object({}),
exampleProperties: moduleSpec?.metadata.exampleProperties || {},
events: moduleSpec?.spec.events || [],
methods: moduleSpec?.spec.methods || [],
});
form = (
<ModuleMetaDataForm

View File

@ -10,19 +10,22 @@ import {
FormErrorMessage,
} from '@chakra-ui/react';
import { RecordEditor } from '@sunmao-ui/editor-sdk';
import { ModuleMethodSpec } from '@sunmao-ui/core';
import { useFormik } from 'formik';
import { observer } from 'mobx-react-lite';
import { EditorServices } from '../../../types';
import { JSONSchema7, JSONSchema7Object } from 'json-schema';
import { JSONSchema7Object } from 'json-schema';
import { CloseIcon } from '@chakra-ui/icons';
import produce from 'immer';
import { CodeEditor } from '../../CodeEditor';
import { css } from '@emotion/css';
export type ModuleMetaDataFormData = {
name: string;
version: string;
stateMap: Record<string, string>;
properties: JSONSchema7;
events: string[];
methods: ModuleMethodSpec[];
exampleProperties: JSONSchema7Object;
};
@ -208,6 +211,25 @@ export const ModuleMetaDataForm: React.FC<ModuleMetaDataFormProps> = observer(
+ Add
</Button>
</FormControl>
<FormControl>
<FormLabel>Methods</FormLabel>
<CodeEditor
className={css`
width: 100%;
height: 120px;
`}
mode="json"
defaultCode={JSON.stringify(formik.values.methods)}
onBlur={v => {
try {
const newMethods = JSON.parse(v);
formik.setFieldValue('methods', newMethods);
formik.submitForm();
} catch {}
}}
needRerenderAfterMount
/>
</FormControl>
</VStack>
);
}

View File

@ -255,7 +255,7 @@ export const ExtractModuleView: React.FC<Props> = ({
if (!genModuleResult || !moduleFormValueRef.current) return;
services.editorStore.appStorage.createModule({
components: genModuleResult.moduleComponentsSchema,
propertySpec: moduleFormValueRef.current.properties,
propertySpec: json2JsonSchema(genModuleResult.exampleProperties),
exampleProperties: genModuleResult.exampleProperties,
events: genModuleResult.eventSpec,
moduleVersion: moduleFormValueRef.current.version,
@ -289,9 +289,9 @@ export const ExtractModuleView: React.FC<Props> = ({
name: componentId,
version: 'custom/v1',
stateMap: result.stateMap,
properties: json2JsonSchema(result.exampleProperties),
events: result.eventSpec,
exampleProperties: result.exampleProperties,
methods: [],
};
setModuleFormInitData(moduleFormData);
moduleFormValueRef.current = moduleFormData;

View File

@ -40,6 +40,7 @@ export const DefaultNewModule: ImplementedRuntimeModule = {
stateMap: {},
events: [],
properties: { type: 'object', properties: {} },
methods: [],
},
impl: [
{

View File

@ -3,6 +3,7 @@ import {
Application,
ComponentSchema,
Module,
ModuleMethodSpec,
parseVersion,
RuntimeModule,
} from '@sunmao-ui/core';
@ -167,6 +168,7 @@ export class AppStorage {
properties,
exampleProperties,
events,
methods,
}: {
version: string;
name: string;
@ -174,6 +176,7 @@ export class AppStorage {
properties: JSONSchema7;
exampleProperties: JSONSchema7Object;
events: string[];
methods: ModuleMethodSpec[];
}
) {
const i = this.modules.findIndex(
@ -186,6 +189,7 @@ export class AppStorage {
draft[i].spec.properties = properties;
draft[i].version = version;
draft[i].spec.events = events;
draft[i].spec.methods = methods;
});
this.setModules(newModules);

View File

@ -53,6 +53,7 @@ export function addModuleId(originModule: Module): Module {
traverse(module.impl);
// value of stateMap is expression, not property
traverse(module.spec.stateMap, true);
traverse(module.spec.methods, true);
});
}
@ -72,6 +73,7 @@ export function removeModuleId(originModule: Module): Module {
traverse(module.impl);
traverse(module.spec.stateMap);
traverse(module.spec.methods);
});
}

View File

@ -16,12 +16,14 @@ import { EventHandlerSpec, ModuleRenderSpec } from '@sunmao-ui/shared';
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
import { initStateAndMethod } from '../../utils/initStateAndMethod';
import { ExpressionError } from '../../services/StateManager';
import { UIMethodPayload } from '../../services/apiService';
type Props = Static<typeof ModuleRenderSpec> & {
evalScope?: Record<string, any>;
services: UIServices;
app: RuntimeApplication;
className?: string;
containerId?: string;
};
export const ModuleRenderer = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
@ -38,7 +40,16 @@ const ModuleRendererContent = React.forwardRef<
HTMLDivElement,
Props & { moduleSpec: ImplementedRuntimeModule }
>((props, ref) => {
const { moduleSpec, properties, handlers, evalScope, services, app, className } = props;
const {
moduleSpec,
properties,
handlers,
evalScope,
services,
app,
className,
containerId,
} = props;
function evalObject<T extends Record<string, any> = Record<string, any>>(
obj: T
@ -57,7 +68,7 @@ const ModuleRendererContent = React.forwardRef<
[moduleSpec]
);
// then eval the template and stateMap of module
// then eval the template, methods and stateMap of module
const evaledStateMap = useMemo(() => {
// stateMap only use state i
return services.stateManager.deepEval(moduleSpec.spec.stateMap, {
@ -66,6 +77,17 @@ const ModuleRendererContent = React.forwardRef<
});
}, [services.stateManager, moduleSpec.spec.stateMap, moduleId]);
// then eval the methods a of module
const evaledMethods = useMemo(() => {
return services.stateManager.deepEval(
{ result: moduleSpec.spec.methods },
{
scopeObject: { $moduleId: moduleId },
overrideScope: true,
}
).result;
}, [services.stateManager, moduleSpec.spec.methods, moduleId]);
const evaledModuleTemplate: RuntimeComponentSchema[] = useDeepCompareMemo(() => {
// here should only eval with evaledProperties, any other key not in evaledProperties should be ignored
// so we can assume that template will not change if evaledProperties is the same
@ -149,6 +171,31 @@ const ModuleRendererContent = React.forwardRef<
};
}, [evalScope, handlers, moduleId, services.apiService, services.stateManager]);
// listen methods calling
useEffect(() => {
const methodHandlers: Array<(payload: UIMethodPayload) => void> = [];
evaledMethods.forEach(methodMap => {
const handler = (payload: UIMethodPayload) => {
if (payload.componentId === containerId && payload.name === methodMap.name) {
services.apiService.send('uiMethod', {
...payload,
componentId: methodMap.componentId,
name: methodMap.componentMethod,
triggerId: containerId,
});
}
};
services.apiService.on('uiMethod', handler);
methodHandlers.push(handler);
});
return () => {
methodHandlers.forEach(h => {
services.apiService.off('uiMethod', h);
});
};
}, [containerId, evaledMethods, services.apiService]);
const result = useMemo(() => {
// Must init components' state, otherwise store cannot listen these components' state changing
initStateAndMethod(services.registry, services.stateManager, evaledModuleTemplate);

View File

@ -77,6 +77,7 @@ export default implementRuntimeComponent({
services={services}
app={app}
ref={elementRef}
containerId={component.id}
/>
);
}

View File

@ -1,23 +1,26 @@
import mitt from 'mitt';
export type ApiService = ReturnType<typeof initApiService>;
/**
* @description: trigger component's method
* @example: { componentId: "btn1", name: "click" }
*/
export type UIMethodPayload = {
componentId: string;
name: string;
triggerId: string;
eventType: string;
parameters?: any;
};
export type ModuleEventPayload = {
fromId: string;
eventType: string;
};
export function initApiService() {
const emitter = mitt<{
/**
* @description: trigger component's method
* @example: { componentId: "btn1", name: "click" }
*/
uiMethod: {
componentId: string;
name: string;
triggerId: string;
eventType: string;
parameters?: any;
};
moduleEvent: {
fromId: string;
eventType: string;
};
uiMethod: UIMethodPayload;
moduleEvent: ModuleEventPayload;
/**
* @description: record merge state info for debug
*/

View File

@ -1,5 +1,5 @@
import { RuntimeTraitSchema } from '@sunmao-ui/core';
import { CoreTraitName, CORE_VERSION } from '../constants';
import { ComponentSchema, parseType, RuntimeTraitSchema } from '@sunmao-ui/core';
import { CoreTraitName, CoreComponentName, CORE_VERSION } from '../constants';
export function isEventTrait(trait: RuntimeTraitSchema) {
return (
@ -7,3 +7,11 @@ export function isEventTrait(trait: RuntimeTraitSchema) {
trait.parsedType.name === CoreTraitName.Event
);
}
export function isModuleContainer(c: ComponentSchema) {
const parsedType = parseType(c.type);
return (
parsedType.version === CORE_VERSION &&
parsedType.name === CoreComponentName.ModuleContainer
);
}