mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
Merge pull request #705 from smartxworks/feat/module-event-method
Support module method
This commit is contained in:
commit
38835afd81
@ -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 || [],
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -40,6 +40,7 @@ export const DefaultNewModule: ImplementedRuntimeModule = {
|
||||
stateMap: {},
|
||||
events: [],
|
||||
properties: { type: 'object', properties: {} },
|
||||
methods: [],
|
||||
},
|
||||
impl: [
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -77,6 +77,7 @@ export default implementRuntimeComponent({
|
||||
services={services}
|
||||
app={app}
|
||||
ref={elementRef}
|
||||
containerId={component.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user