mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
feat(editor): add extract module feature
This commit is contained in:
parent
145df0da75
commit
10bc79556f
@ -14,6 +14,8 @@ import {
|
||||
CoreWidgetName,
|
||||
generateDefaultValueFromSpec,
|
||||
MountEvents,
|
||||
GLOBAL_MODULE_ID,
|
||||
ModuleEventMethodSpec,
|
||||
} from '@sunmao-ui/shared';
|
||||
import { JSONSchema7Object } from 'json-schema';
|
||||
import { PREVENT_POPOVER_WIDGET_CLOSE_CLASS } from '../../constants/widget';
|
||||
@ -32,7 +34,7 @@ declare module '../../types/widget' {
|
||||
export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(props => {
|
||||
const { value, path, level, component, spec, services, onChange } = props;
|
||||
const { registry, editorStore, appModelManager } = services;
|
||||
const { components } = editorStore;
|
||||
const { components, currentEditingTarget } = editorStore;
|
||||
const utilMethods = useMemo(() => registry.getAllUtilMethods(), [registry]);
|
||||
const [methods, setMethods] = useState<string[]>([]);
|
||||
|
||||
@ -62,9 +64,22 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
|
||||
},
|
||||
[registry]
|
||||
);
|
||||
|
||||
const eventTypes = useMemo(() => {
|
||||
return [...registry.getComponentByType(component.type).spec.events, ...MountEvents];
|
||||
}, [component.type, registry]);
|
||||
let moduleEvents: string[] = [];
|
||||
if (component.type === 'core/v1/moduleContainer') {
|
||||
// if component is moduleContainer, add module events to it
|
||||
const moduleType = component.properties.type as string;
|
||||
const moduleSpec = registry.getModuleByType(moduleType);
|
||||
moduleEvents = moduleSpec.spec.events;
|
||||
}
|
||||
return [
|
||||
...registry.getComponentByType(component.type).spec.events,
|
||||
...moduleEvents,
|
||||
...MountEvents,
|
||||
];
|
||||
}, [component.properties.type, component.type, registry]);
|
||||
|
||||
const hasParams = useMemo(
|
||||
() => Object.keys(formik.values.method.parameters ?? {}).length,
|
||||
[formik.values.method.parameters]
|
||||
@ -79,6 +94,8 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
|
||||
const targetMethod = registry.getUtilMethodByType(methodType)!;
|
||||
|
||||
spec = targetMethod.spec.parameters;
|
||||
} else if (componentId === GLOBAL_MODULE_ID) {
|
||||
spec = ModuleEventMethodSpec;
|
||||
} else {
|
||||
const targetComponent = appModelManager.appModel.getComponentById(componentId);
|
||||
const targetMethod = (findMethodsByComponent(targetComponent) ?? []).find(
|
||||
@ -140,19 +157,42 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
|
||||
utilMethod => `${utilMethod.version}/${utilMethod.metadata.name}`
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
componentId === GLOBAL_MODULE_ID &&
|
||||
currentEditingTarget.kind === 'module'
|
||||
) {
|
||||
// if user is editing module, show the events of module spec as method
|
||||
const moduleType = `${currentEditingTarget.version}/${currentEditingTarget.name}`;
|
||||
let methodNames: string[] = [];
|
||||
if (moduleType) {
|
||||
const moduleSpec = services.registry.getModuleByType(moduleType);
|
||||
|
||||
if (moduleSpec) {
|
||||
methodNames = moduleSpec.spec.events;
|
||||
}
|
||||
}
|
||||
setMethods(methodNames);
|
||||
} else {
|
||||
// if user is editing application, show methods of component
|
||||
const component = components.find(c => c.id === componentId);
|
||||
|
||||
if (component) {
|
||||
const methodNames: string[] = findMethodsByComponent(component).map(
|
||||
({ name }) => name
|
||||
);
|
||||
|
||||
setMethods(methodNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
[components, utilMethods, findMethodsByComponent]
|
||||
[
|
||||
currentEditingTarget.kind,
|
||||
currentEditingTarget.version,
|
||||
currentEditingTarget.name,
|
||||
utilMethods,
|
||||
services.registry,
|
||||
components,
|
||||
findMethodsByComponent,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -235,6 +275,11 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetType>> = observer(prop
|
||||
style={{ width: '100%' }}
|
||||
value={formik.values.componentId === '' ? undefined : formik.values.componentId}
|
||||
>
|
||||
{currentEditingTarget.kind === 'module' ? (
|
||||
<ComponentTargetSelect.Option key={GLOBAL_MODULE_ID} value={GLOBAL_MODULE_ID}>
|
||||
{GLOBAL_MODULE_ID}
|
||||
</ComponentTargetSelect.Option>
|
||||
) : undefined}
|
||||
{[{ id: GLOBAL_UTIL_METHOD_ID }].concat(components).map(c => (
|
||||
<ComponentTargetSelect.Option key={c.id} value={c.id}>
|
||||
{c.id}
|
||||
|
@ -1,21 +1,11 @@
|
||||
import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
|
||||
import {
|
||||
toNumber,
|
||||
isString,
|
||||
isNumber,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
isObject,
|
||||
isUndefined,
|
||||
isNull,
|
||||
debounce,
|
||||
} from 'lodash';
|
||||
import { toNumber, debounce } from 'lodash';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { WidgetProps } from '../../types/widget';
|
||||
import { implementWidget } from '../../utils/widget';
|
||||
import { ExpressionEditor, ExpressionEditorHandle } from '../Form';
|
||||
import { isExpression } from '../../utils/validator';
|
||||
import { getTypeString } from '../../utils/type';
|
||||
import { getType, getTypeString, Types } from '../../utils/type';
|
||||
import { ValidateFunction } from 'ajv';
|
||||
import { ExpressionError } from '@sunmao-ui/runtime';
|
||||
import { CORE_VERSION, CoreWidgetName, initAjv } from '@sunmao-ui/shared';
|
||||
@ -26,30 +16,6 @@ export function isNumeric(x: string | number) {
|
||||
}
|
||||
|
||||
// highly inspired by appsmith
|
||||
export enum Types {
|
||||
STRING = 'STRING',
|
||||
NUMBER = 'NUMBER',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
OBJECT = 'OBJECT',
|
||||
ARRAY = 'ARRAY',
|
||||
FUNCTION = 'FUNCTION',
|
||||
UNDEFINED = 'UNDEFINED',
|
||||
NULL = 'NULL',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export const getType = (value: unknown) => {
|
||||
if (isString(value)) return Types.STRING;
|
||||
if (isNumber(value)) return Types.NUMBER;
|
||||
if (isBoolean(value)) return Types.BOOLEAN;
|
||||
if (Array.isArray(value)) return Types.ARRAY;
|
||||
if (isFunction(value)) return Types.FUNCTION;
|
||||
if (isObject(value)) return Types.OBJECT;
|
||||
if (isUndefined(value)) return Types.UNDEFINED;
|
||||
if (isNull(value)) return Types.NULL;
|
||||
return Types.UNKNOWN;
|
||||
};
|
||||
|
||||
function generateTypeDef(
|
||||
obj: any
|
||||
): string | Record<string, string | Record<string, unknown>> {
|
||||
@ -164,13 +130,14 @@ export const ExpressionWidget: React.FC<WidgetProps<ExpressionWidgetType>> = pro
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const editorRef = useRef<ExpressionEditorHandle>(null);
|
||||
const validateFuncRef = useRef<ValidateFunction | null>(null);
|
||||
const slotTrait = useMemo(
|
||||
() =>
|
||||
component.traits.find(trait =>
|
||||
const slotTrait = useMemo(() => {
|
||||
if (component.traits) {
|
||||
return component.traits.find(trait =>
|
||||
['core/v1/slot', 'core/v2/slot'].includes(trait.type)
|
||||
),
|
||||
[component.traits]
|
||||
);
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}, [component]);
|
||||
const $slot = useMemo(
|
||||
() =>
|
||||
slotTrait
|
||||
|
@ -6,9 +6,8 @@ import { SpecWidget } from './SpecWidget';
|
||||
import { CORE_VERSION, CoreWidgetName, isJSONSchema } from '@sunmao-ui/shared';
|
||||
import { css } from '@emotion/css';
|
||||
import { mapValues } from 'lodash';
|
||||
import { Type, TSchema } from '@sinclair/typebox';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { getType, Types } from './ExpressionWidget';
|
||||
import { json2JsonSchema } from '../../utils/type';
|
||||
|
||||
const LabelStyle = css`
|
||||
font-weight: normal;
|
||||
@ -23,34 +22,6 @@ declare module '../../types/widget' {
|
||||
}
|
||||
}
|
||||
|
||||
const genSpec = (type: Types, target: any): TSchema => {
|
||||
switch (type) {
|
||||
case Types.ARRAY: {
|
||||
const arrayType = getType(target[0]);
|
||||
return Type.Array(genSpec(arrayType, target[0]));
|
||||
}
|
||||
case Types.OBJECT: {
|
||||
const objType: Record<string, any> = {};
|
||||
Object.keys(target).forEach(k => {
|
||||
const type = getType(target[k]);
|
||||
objType[k] = genSpec(type, target[k]);
|
||||
});
|
||||
return Type.Object(objType);
|
||||
}
|
||||
case Types.STRING:
|
||||
return Type.String();
|
||||
case Types.NUMBER:
|
||||
return Type.Number();
|
||||
case Types.BOOLEAN:
|
||||
return Type.Boolean();
|
||||
case Types.NULL:
|
||||
case Types.UNDEFINED:
|
||||
return Type.Any();
|
||||
default:
|
||||
return Type.Any();
|
||||
}
|
||||
};
|
||||
|
||||
export const ModuleWidget: React.FC<WidgetProps<ModuleWidgetType>> = props => {
|
||||
const { component, value, spec, services, path, level, onChange } = props;
|
||||
const { registry } = services;
|
||||
@ -100,13 +71,11 @@ export const ModuleWidget: React.FC<WidgetProps<ModuleWidgetType>> = props => {
|
||||
const modulePropertiesSpec = useMemo<JSONSchema7>(() => {
|
||||
const obj = mapValues(module?.metadata.exampleProperties, p => {
|
||||
const result = services.stateManager.deepEval(p);
|
||||
const type = getType(result);
|
||||
const spec = genSpec(type, result);
|
||||
|
||||
return spec;
|
||||
return json2JsonSchema(result);
|
||||
});
|
||||
|
||||
return Type.Object(obj);
|
||||
return { type: 'object', properties: obj };
|
||||
}, [module?.metadata.exampleProperties, services.stateManager]);
|
||||
|
||||
return (
|
||||
|
@ -7,6 +7,11 @@ export interface EditorServicesInterface extends UIServices {
|
||||
registry: RegistryInterface;
|
||||
editorStore: {
|
||||
components: ComponentSchema[];
|
||||
currentEditingTarget: {
|
||||
kind: 'app' | 'module';
|
||||
version: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
appModelManager: {
|
||||
appModel: any;
|
||||
|
@ -1,7 +1,7 @@
|
||||
type OperationType =
|
||||
| 'createComponent'
|
||||
| 'removeComponent'
|
||||
| 'modifyComponentProperty'
|
||||
| 'modifyComponentProperties'
|
||||
| 'modifyComponentId'
|
||||
| 'adjustComponentOrder'
|
||||
| 'createTrait'
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './widget';
|
||||
export * from './validator';
|
||||
export * from './type';
|
||||
|
@ -1,3 +1,15 @@
|
||||
import {
|
||||
isString,
|
||||
isNumber,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
isObject,
|
||||
isUndefined,
|
||||
isNull,
|
||||
} from 'lodash';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { TSchema, Type } from '@sinclair/typebox';
|
||||
|
||||
const TypeMap = {
|
||||
undefined: 'Undefined',
|
||||
string: 'String',
|
||||
@ -18,3 +30,59 @@ export function getTypeString(value: any) {
|
||||
return TypeMap[typeof value];
|
||||
}
|
||||
}
|
||||
|
||||
export enum Types {
|
||||
STRING = 'STRING',
|
||||
NUMBER = 'NUMBER',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
OBJECT = 'OBJECT',
|
||||
ARRAY = 'ARRAY',
|
||||
FUNCTION = 'FUNCTION',
|
||||
UNDEFINED = 'UNDEFINED',
|
||||
NULL = 'NULL',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export const getType = (value: unknown) => {
|
||||
if (isString(value)) return Types.STRING;
|
||||
if (isNumber(value)) return Types.NUMBER;
|
||||
if (isBoolean(value)) return Types.BOOLEAN;
|
||||
if (Array.isArray(value)) return Types.ARRAY;
|
||||
if (isFunction(value)) return Types.FUNCTION;
|
||||
if (isObject(value)) return Types.OBJECT;
|
||||
if (isUndefined(value)) return Types.UNDEFINED;
|
||||
if (isNull(value)) return Types.NULL;
|
||||
return Types.UNKNOWN;
|
||||
};
|
||||
|
||||
const genSpec = (type: Types, target: any): TSchema => {
|
||||
switch (type) {
|
||||
case Types.ARRAY: {
|
||||
const arrayType = getType(target[0]);
|
||||
return Type.Array(genSpec(arrayType, target[0]));
|
||||
}
|
||||
case Types.OBJECT: {
|
||||
const objType: Record<string, any> = {};
|
||||
Object.keys(target).forEach(k => {
|
||||
const type = getType(target[k]);
|
||||
objType[k] = genSpec(type, target[k]);
|
||||
});
|
||||
return Type.Object(objType);
|
||||
}
|
||||
case Types.STRING:
|
||||
return Type.String();
|
||||
case Types.NUMBER:
|
||||
return Type.Number();
|
||||
case Types.BOOLEAN:
|
||||
return Type.Boolean();
|
||||
case Types.NULL:
|
||||
case Types.UNDEFINED:
|
||||
return Type.Any();
|
||||
default:
|
||||
return Type.Any();
|
||||
}
|
||||
};
|
||||
export const json2JsonSchema = (value: any): JSONSchema7 => {
|
||||
const type = getType(value);
|
||||
return genSpec(type, value) as JSONSchema7;
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
} from './IAppModel';
|
||||
import { TraitModel } from './TraitModel';
|
||||
import { FieldModel } from './FieldModel';
|
||||
import { AppModel } from './AppModel';
|
||||
|
||||
const SlotTraitType: TraitType = `${CORE_VERSION}/${CoreTraitName.Slot}` as TraitType;
|
||||
const SlotTraitTypeV2: TraitType = `core/v2/${CoreTraitName.Slot}` as TraitType;
|
||||
@ -304,6 +305,19 @@ export class ComponentModel implements IComponentModel {
|
||||
this._isDirty = true;
|
||||
}
|
||||
|
||||
removeSlotTrait() {
|
||||
if (this._slotTrait) {
|
||||
this.removeTrait(this._slotTrait.id);
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new AppModel(
|
||||
this.allComponents.map(c => c.toSchema()),
|
||||
this.registry
|
||||
).getComponentById(this.id)!;
|
||||
}
|
||||
|
||||
private traverseTree(cb: (c: IComponentModel) => void) {
|
||||
function traverse(root: IComponentModel) {
|
||||
cb(root);
|
||||
|
@ -98,6 +98,7 @@ export interface IComponentModel {
|
||||
_isDirty: boolean;
|
||||
_slotTrait: ITraitModel | null;
|
||||
toSchema(): ComponentSchema;
|
||||
clone(): IComponentModel;
|
||||
updateComponentProperty: (property: string, value: unknown) => void;
|
||||
// move component from old parent to new parent(or top level if parent is undefined).
|
||||
appendTo: (parent?: IComponentModel, slot?: SlotName) => void;
|
||||
@ -110,6 +111,7 @@ export interface IComponentModel {
|
||||
removeTrait: (traitId: TraitId) => void;
|
||||
updateTraitProperties: (traitId: TraitId, properties: Record<string, unknown>) => void;
|
||||
updateSlotTrait: (parent: ComponentId, slot: SlotName) => void;
|
||||
removeSlotTrait: () => void;
|
||||
removeChild: (child: IComponentModel) => void;
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
onChange={newFormData => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'modifyComponentProperty', {
|
||||
genOperation(registry, 'modifyComponentProperties', {
|
||||
componentId: selectedComponentId,
|
||||
properties: newFormData,
|
||||
})
|
||||
|
@ -85,9 +85,12 @@ export const Editor: React.FC<Props> = observer(
|
||||
}
|
||||
}, [isDisplayApp]);
|
||||
const onPreview = useCallback(() => setPreview(true), []);
|
||||
const onRightTabChange = useCallback(activatedTab => {
|
||||
setToolMenuTab(activatedTab);
|
||||
}, []);
|
||||
const onRightTabChange = useCallback(
|
||||
activatedTab => {
|
||||
setToolMenuTab(activatedTab);
|
||||
},
|
||||
[setToolMenuTab]
|
||||
);
|
||||
|
||||
const renderMain = () => {
|
||||
const appBox = (
|
||||
|
@ -58,6 +58,7 @@ export const Explorer: React.FC<Props> = ({ services }) => {
|
||||
setCurrentVersion={setCurrentVersion}
|
||||
setCurrentName={setCurrentName}
|
||||
services={services}
|
||||
onClose={() => setIsEditingMode(false)}
|
||||
/>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { Button, ButtonGroup, Spacer, VStack } from '@chakra-ui/react';
|
||||
import { AppMetaDataForm, AppMetaDataFormData } from './AppMetaDataForm';
|
||||
import { ModuleMetaDataForm, ModuleMetaDataFormData } from './ModuleMetaDataForm';
|
||||
import { EditorServices } from '../../../types';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
formType: 'app' | 'module';
|
||||
@ -13,15 +14,49 @@ type Props = {
|
||||
setCurrentVersion?: (version: string) => void;
|
||||
setCurrentName?: (name: string) => void;
|
||||
services: EditorServices;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ExplorerForm: React.FC<Props> = observer(
|
||||
({ formType, version, name, setCurrentVersion, setCurrentName, services }) => {
|
||||
({ formType, version, name, setCurrentVersion, setCurrentName, services, onClose }) => {
|
||||
const { editorStore } = services;
|
||||
const onSubmit = (value: AppMetaDataFormData | ModuleMetaDataFormData) => {
|
||||
setCurrentVersion?.(value.version);
|
||||
setCurrentName?.(value.name);
|
||||
const newModuleMetaDataRef = useRef<ModuleMetaDataFormData | undefined>();
|
||||
const newAppMetaDataRef = useRef<AppMetaDataFormData | undefined>();
|
||||
const onModuleMetaDataChange = (value: ModuleMetaDataFormData) => {
|
||||
newModuleMetaDataRef.current = value;
|
||||
};
|
||||
const onAppMetaDataChange = (value: AppMetaDataFormData) => {
|
||||
newAppMetaDataRef.current = value;
|
||||
};
|
||||
const saveModuleMetaData = () => {
|
||||
if (!newModuleMetaDataRef.current) return;
|
||||
editorStore.appStorage.saveModuleMetaData(
|
||||
{ originName: name, originVersion: version },
|
||||
newModuleMetaDataRef.current
|
||||
);
|
||||
editorStore.setModuleDependencies(newModuleMetaDataRef.current.exampleProperties);
|
||||
setCurrentVersion?.(newModuleMetaDataRef.current.version);
|
||||
setCurrentName?.(newModuleMetaDataRef.current.name);
|
||||
};
|
||||
const saveAppMetaData = () => {
|
||||
if (!newAppMetaDataRef.current) return;
|
||||
editorStore.appStorage.saveAppMetaData(newAppMetaDataRef.current);
|
||||
setCurrentVersion?.(newAppMetaDataRef.current.version);
|
||||
setCurrentName?.(newAppMetaDataRef.current.name);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
switch (formType) {
|
||||
case 'app':
|
||||
saveAppMetaData();
|
||||
break;
|
||||
case 'module':
|
||||
saveModuleMetaData();
|
||||
break;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
let form;
|
||||
switch (formType) {
|
||||
case 'app':
|
||||
@ -30,33 +65,49 @@ export const ExplorerForm: React.FC<Props> = observer(
|
||||
version,
|
||||
};
|
||||
form = (
|
||||
<AppMetaDataForm data={appMetaData} services={services} onSubmit={onSubmit} />
|
||||
<AppMetaDataForm
|
||||
data={appMetaData}
|
||||
services={services}
|
||||
onSubmit={onAppMetaDataChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'module':
|
||||
// Don't get from registry, because module from registry has __$moduleId
|
||||
const moduleSpec = editorStore.appStorage.modules.find(
|
||||
m => m.version === version && m.metadata.name === name
|
||||
)!;
|
||||
const moduleMetaData = {
|
||||
|
||||
const moduleMetaData = cloneDeep({
|
||||
name,
|
||||
version,
|
||||
stateMap: moduleSpec?.spec.stateMap || {},
|
||||
properties: moduleSpec?.spec.properties || Type.Object({}),
|
||||
exampleProperties: moduleSpec?.metadata.exampleProperties || {},
|
||||
events: moduleSpec?.spec.events || [],
|
||||
};
|
||||
});
|
||||
form = (
|
||||
<ModuleMetaDataForm
|
||||
services={services}
|
||||
initData={moduleMetaData}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={onModuleMetaDataChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack w="full" alignItems="start">
|
||||
<VStack h="full" w="full" alignItems="end">
|
||||
{form}
|
||||
<Spacer />
|
||||
<ButtonGroup flex="0 0">
|
||||
<Button variant="outline" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="blue" onClick={() => onSave()}>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ type ModuleMetaDataFormProps = {
|
||||
initData: ModuleMetaDataFormData;
|
||||
services: EditorServices;
|
||||
onSubmit?: (value: ModuleMetaDataFormData) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const genEventsName = (events: string[]) => {
|
||||
@ -84,17 +85,9 @@ const EventInput: React.FC<{
|
||||
|
||||
export const ModuleMetaDataForm: React.FC<ModuleMetaDataFormProps> = observer(
|
||||
({ initData, services, onSubmit: onSubmitForm }) => {
|
||||
const { editorStore } = services;
|
||||
|
||||
const onSubmit = (value: ModuleMetaDataFormData) => {
|
||||
editorStore.appStorage.saveModuleMetaData(
|
||||
{ originName: initData.name, originVersion: initData.version },
|
||||
value
|
||||
);
|
||||
editorStore.setModuleDependencies(value.exampleProperties);
|
||||
onSubmitForm?.(value);
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: initData,
|
||||
onSubmit,
|
||||
@ -158,7 +151,7 @@ export const ModuleMetaDataForm: React.FC<ModuleMetaDataFormProps> = observer(
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Properties</FormLabel>
|
||||
<FormLabel>Example Properties</FormLabel>
|
||||
<RecordEditor
|
||||
services={services}
|
||||
value={formik.values.exampleProperties}
|
||||
|
@ -101,7 +101,7 @@ export const ExplorerTree: React.FC<ExplorerTreeProps> = observer(
|
||||
aria-label="create module"
|
||||
size="xs"
|
||||
icon={<AddIcon />}
|
||||
onClick={() => editorStore.appStorage.createModule()}
|
||||
onClick={() => editorStore.appStorage.createModule({})}
|
||||
/>
|
||||
</HStack>
|
||||
{moduleItems}
|
||||
|
@ -0,0 +1,98 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Heading,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
VStack,
|
||||
Link,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { EditorServices } from '../../types';
|
||||
import { InsideMethodRelation } from './type';
|
||||
import { Placeholder } from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
methodRelations: InsideMethodRelation[];
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const ExtractModuleEventForm: React.FC<Props> = ({
|
||||
methodRelations,
|
||||
services,
|
||||
}) => {
|
||||
const { editorStore } = services;
|
||||
|
||||
const idLink = useCallback(
|
||||
(id: string) => {
|
||||
return (
|
||||
<Link
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
editorStore.setSelectedComponentId(id);
|
||||
}}
|
||||
>
|
||||
{id}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
[editorStore]
|
||||
);
|
||||
|
||||
let content = (
|
||||
<Placeholder text={`No event handler calls outside components' method.`} />
|
||||
);
|
||||
|
||||
if (methodRelations.length) {
|
||||
content = (
|
||||
<Table
|
||||
size="sm"
|
||||
border="1px solid"
|
||||
borderColor="gray.100"
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Source</Th>
|
||||
<Th>Event</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Method</Th>
|
||||
<Th>Module Event Name</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{methodRelations.map((d, i) => {
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>
|
||||
<Text color="blue.500">{d.source}</Text>
|
||||
</Td>
|
||||
<Td>{d.event}</Td>
|
||||
<Td>{idLink(d.target)}</Td>
|
||||
<Td>{d.method}</Td>
|
||||
<Td fontWeight="bold">{d.source + d.event}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack width="full" alignItems="start">
|
||||
<Heading size="md">Module Events</Heading>
|
||||
<Text>
|
||||
{`These components' event handlers call outside components' methods.
|
||||
These events will be convert automatically to module's events and exposed to outside components.
|
||||
You don't have to do anything.`}
|
||||
</Text>
|
||||
{content}
|
||||
</VStack>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalHeader,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { EditorServices } from '../../types';
|
||||
import { ExtractModuleView } from './ExtractModuleView';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
onClose: () => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
export const ExtractModuleModal: React.FC<Props> = ({
|
||||
componentId,
|
||||
onClose,
|
||||
services,
|
||||
}) => {
|
||||
return (
|
||||
<Modal onClose={onClose} isOpen>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth="60vw">
|
||||
<ModalHeader>
|
||||
Extract component{' '}
|
||||
<Text display="inline" color="red.500">
|
||||
{componentId}
|
||||
</Text>{' '}
|
||||
to module
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody flex="1 1 auto" height="75vh" alignItems="start" overflow="auto">
|
||||
<ExtractModuleView
|
||||
componentId={componentId}
|
||||
services={services}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,181 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Heading,
|
||||
VStack,
|
||||
Button,
|
||||
HStack,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Tooltip,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { groupBy, uniq } from 'lodash';
|
||||
import { EditorServices } from '../../types';
|
||||
import { InsideExpRelation, RefTreatmentMap, RefTreatment } from './type';
|
||||
import { RelationshipModal } from '../RelationshipModal';
|
||||
import { Placeholder } from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
insideExpRelations: InsideExpRelation[];
|
||||
onChange: (value: RefTreatmentMap) => void;
|
||||
services: EditorServices;
|
||||
};
|
||||
|
||||
const RadioOptions = [
|
||||
{
|
||||
value: RefTreatment.keep,
|
||||
label: 'Keep outside',
|
||||
desc: `Keep this outside and pass this component's state into module by properties.`,
|
||||
},
|
||||
{
|
||||
value: RefTreatment.move,
|
||||
label: 'Move into module',
|
||||
desc: 'Move this component into module and delete it outside.',
|
||||
},
|
||||
{
|
||||
value: RefTreatment.duplicate,
|
||||
label: 'Dupliacte',
|
||||
desc: 'Copy and paste this component into module and keep it outside at the same time.',
|
||||
},
|
||||
{
|
||||
value: RefTreatment.ignore,
|
||||
label: 'Ignore',
|
||||
desc: `Don't do anything.`,
|
||||
},
|
||||
];
|
||||
|
||||
export const ExtractModulePropertyForm: React.FC<Props> = ({
|
||||
insideExpRelations,
|
||||
onChange,
|
||||
services,
|
||||
}) => {
|
||||
const [showRelationId, setShowRelationId] = useState('');
|
||||
const [value, setValue] = useState<RefTreatmentMap>({});
|
||||
|
||||
const [relationGroups, refIds] = useMemo(
|
||||
() => [
|
||||
groupBy(insideExpRelations, r => r.componentId),
|
||||
uniq(insideExpRelations.map(r => r.componentId)),
|
||||
],
|
||||
[insideExpRelations]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const map: RefTreatmentMap = {};
|
||||
refIds.forEach(r => {
|
||||
map[r] = RefTreatment.keep;
|
||||
});
|
||||
setValue(map);
|
||||
}, [refIds]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(value);
|
||||
}, [onChange, value]);
|
||||
|
||||
let content = <Placeholder text={`No component uses outside components' state.`} />;
|
||||
if (refIds.length) {
|
||||
content = (
|
||||
<VStack width="full" spacing={4}>
|
||||
{Object.keys(value).map(id => {
|
||||
return (
|
||||
<VStack key={id} width="full">
|
||||
<FormControl as="fieldset" width="full">
|
||||
<FormLabel>
|
||||
<Button
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
onClick={() => setShowRelationId(id)}
|
||||
>
|
||||
{id}
|
||||
</Button>
|
||||
</FormLabel>
|
||||
|
||||
<RadioGroup
|
||||
onChange={newValue => {
|
||||
const next = { ...value, [id]: newValue as any };
|
||||
setValue(next);
|
||||
}}
|
||||
value={value[id]}
|
||||
>
|
||||
<HStack>
|
||||
{RadioOptions.map(o => (
|
||||
<Radio key={o.value} value={o.value} cursor="pointer">
|
||||
<Tooltip cursor="pointer" label={o.desc}>
|
||||
{o.label}
|
||||
</Tooltip>
|
||||
</Radio>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Table
|
||||
size="sm"
|
||||
border="1px solid"
|
||||
borderColor="gray.100"
|
||||
width="full"
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Component Id</Th>
|
||||
<Th>Property Key</Th>
|
||||
<Th>Expression</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{relationGroups[id].map((r, i) => {
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>
|
||||
<Text color="red.500">
|
||||
{r.source}
|
||||
{r.traitType ? `-${r.traitType}` : ''}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td>{r.key}</Td>
|
||||
<Td>{r.exp}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</VStack>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
const relationshipViewModal = showRelationId ? (
|
||||
<RelationshipModal
|
||||
componentId={showRelationId}
|
||||
services={services}
|
||||
onClose={() => setShowRelationId('')}
|
||||
/>
|
||||
) : null;
|
||||
return (
|
||||
<>
|
||||
<VStack width="full" alignItems="start">
|
||||
<Heading size="md">Module Properties</Heading>
|
||||
<Text>
|
||||
These components are used by the components of module, you have to decide how to
|
||||
treat them.
|
||||
</Text>
|
||||
{content}
|
||||
</VStack>
|
||||
{relationshipViewModal}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,103 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Heading,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
VStack,
|
||||
Input,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { OutsideExpRelation, OutsideExpRelationWithState } from './type';
|
||||
import { Placeholder } from './Placeholder';
|
||||
import produce from 'immer';
|
||||
|
||||
type Props = {
|
||||
outsideExpRelations: OutsideExpRelation[];
|
||||
onChange: (value: OutsideExpRelationWithState[]) => void;
|
||||
};
|
||||
|
||||
export const ExtractModuleStateForm: React.FC<Props> = ({
|
||||
outsideExpRelations,
|
||||
onChange,
|
||||
}) => {
|
||||
const [value, setValue] = useState<OutsideExpRelationWithState[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const newValue = outsideExpRelations.map(r => {
|
||||
return {
|
||||
...r,
|
||||
stateName: `${r.relyOn}${r.valuePath}`,
|
||||
};
|
||||
});
|
||||
setValue(newValue);
|
||||
}, [outsideExpRelations]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(value);
|
||||
}, [onChange, value]);
|
||||
|
||||
let content = (
|
||||
<Placeholder text={`No outside component uses in-module components' state.`} />
|
||||
);
|
||||
if (outsideExpRelations.length) {
|
||||
content = (
|
||||
<Table
|
||||
size="sm"
|
||||
border="1px solid"
|
||||
borderColor="gray.100"
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Component</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>
|
||||
<Text color="blue.500">{d.componentId}</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack width="full" alignItems="start">
|
||||
<Heading size="md">Module State</Heading>
|
||||
<Text>
|
||||
{`These outside components used in-module components' state.
|
||||
You have to give these expression a new name which will become the state exposed by module.`}
|
||||
</Text>
|
||||
{content}
|
||||
</VStack>
|
||||
);
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { List, ListItem, ListIcon } from '@chakra-ui/react';
|
||||
import { CheckCircleIcon, SettingsIcon } from '@chakra-ui/icons';
|
||||
|
||||
type Props = {
|
||||
activeIndex: number;
|
||||
};
|
||||
|
||||
export const ExtractModuleStep: React.FC<Props> = ({ activeIndex }) => {
|
||||
return (
|
||||
<List spacing={3} width="150px" flex="0 0 auto">
|
||||
<ListItem
|
||||
fontWeight={activeIndex === 0 ? 'bold' : 'normal'}
|
||||
color={activeIndex > 0 ? 'gray.500' : 'gray.900'}
|
||||
>
|
||||
<ListIcon as={activeIndex > 0 ? CheckCircleIcon : SettingsIcon} />
|
||||
Module Properties
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
fontWeight={activeIndex === 1 ? 'bold' : 'normal'}
|
||||
color={activeIndex > 1 ? 'gray.500' : 'gray.900'}
|
||||
>
|
||||
<ListIcon as={activeIndex > 1 ? CheckCircleIcon : SettingsIcon} />
|
||||
Module State
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
fontWeight={activeIndex === 2 ? 'bold' : 'normal'}
|
||||
color={activeIndex > 2 ? 'gray.500' : 'gray.900'}
|
||||
>
|
||||
<ListIcon as={activeIndex > 2 ? CheckCircleIcon : SettingsIcon} />
|
||||
Module Events
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
fontWeight={activeIndex === 3 ? 'bold' : 'normal'}
|
||||
color={activeIndex > 3 ? 'gray.500' : 'gray.900'}
|
||||
>
|
||||
<ListIcon as={activeIndex > 3 ? CheckCircleIcon : SettingsIcon} />
|
||||
Preview
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
@ -0,0 +1,362 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Button,
|
||||
HStack,
|
||||
Box,
|
||||
ButtonGroup,
|
||||
Spacer,
|
||||
Heading,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { EventHandlerSpec, GLOBAL_MODULE_ID } from '@sunmao-ui/shared';
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import { set, uniq } from 'lodash';
|
||||
import { EditorServices } from '../../types';
|
||||
import { ComponentId } from '../../AppModel/IAppModel';
|
||||
import {
|
||||
RefTreatmentMap,
|
||||
OutsideExpRelationWithState,
|
||||
RefTreatment,
|
||||
InsideExpRelation,
|
||||
InsideMethodRelation,
|
||||
} from './type';
|
||||
import { ExtractModuleStep } from './ExtractModuleStep';
|
||||
import { ExtractModulePropertyForm } from './ExtractModulePropertyForm';
|
||||
import { ExtractModuleStateForm } from './ExtractModuleStateForm';
|
||||
import { ExtractModuleEventForm } from './ExtractModuleEventForm';
|
||||
import {
|
||||
ModuleMetaDataForm,
|
||||
ModuleMetaDataFormData,
|
||||
} from '../Explorer/ExplorerForm/ModuleMetaDataForm';
|
||||
import { toJS } from 'mobx';
|
||||
import { json2JsonSchema } from '@sunmao-ui/editor-sdk';
|
||||
import { genOperation } from '../../operations';
|
||||
import { getInsideRelations, getOutsideExpRelations } from './utils';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
services: EditorServices;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type InsideRelations = {
|
||||
exp: InsideExpRelation[];
|
||||
method: InsideMethodRelation[];
|
||||
};
|
||||
|
||||
export const ExtractModuleView: React.FC<Props> = ({
|
||||
componentId,
|
||||
services,
|
||||
onClose,
|
||||
}) => {
|
||||
const { appModelManager } = services;
|
||||
const { appModel } = appModelManager;
|
||||
const refTreatmentMap = useRef<RefTreatmentMap>({});
|
||||
const outsideExpRelationsValueRef = useRef<OutsideExpRelationWithState[]>([]);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [genModuleResult, serGenModuleResult] = useState<
|
||||
ReturnType<typeof genModule> | undefined
|
||||
>();
|
||||
const [moduleFormInitData, setModuleFormInitData] = useState<
|
||||
ModuleMetaDataFormData | undefined
|
||||
>();
|
||||
const moduleFormValueRef = useRef<ModuleMetaDataFormData | undefined>();
|
||||
|
||||
const { insideExpRelations, methodRelations, outsideExpRelations } = useMemo(() => {
|
||||
const root = appModel.getComponentById(componentId as ComponentId)!;
|
||||
const moduleComponents = root.allComponents;
|
||||
const insideRelations = moduleComponents.reduce<InsideRelations>(
|
||||
(res, c) => {
|
||||
const { expressionRelations, methodRelations } = getInsideRelations(
|
||||
c,
|
||||
moduleComponents
|
||||
);
|
||||
res.exp = res.exp.concat(expressionRelations);
|
||||
res.method = res.method.concat(methodRelations);
|
||||
return res;
|
||||
},
|
||||
{ exp: [], method: [] }
|
||||
);
|
||||
const outsideExpRelations = getOutsideExpRelations(appModel.allComponents, root);
|
||||
return {
|
||||
insideExpRelations: insideRelations.exp,
|
||||
methodRelations: insideRelations.method,
|
||||
outsideExpRelations,
|
||||
};
|
||||
}, [appModel, componentId]);
|
||||
|
||||
const genModule = useCallback(() => {
|
||||
const exampleProperties: Record<string, string> = {};
|
||||
const moduleContainerProperties: Record<string, string> = {};
|
||||
let toMoveComponentIds: string[] = [];
|
||||
let toDeleteComponentIds: string[] = [];
|
||||
insideExpRelations.forEach(relation => {
|
||||
switch (refTreatmentMap.current[relation.componentId]) {
|
||||
case RefTreatment.move:
|
||||
toMoveComponentIds.push(relation.componentId);
|
||||
toDeleteComponentIds.push(relation.componentId);
|
||||
break;
|
||||
case RefTreatment.keep:
|
||||
moduleContainerProperties[relation.componentId] = `{{${relation.componentId}}}`;
|
||||
const value = toJS(services.stateManager.store[relation.componentId]);
|
||||
if (typeof value === 'string') {
|
||||
exampleProperties[relation.componentId] = value;
|
||||
} else {
|
||||
// save value as expression
|
||||
exampleProperties[relation.componentId] = `{{${JSON.stringify(value)}}}`;
|
||||
}
|
||||
break;
|
||||
case RefTreatment.duplicate:
|
||||
toMoveComponentIds.push(relation.componentId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
toMoveComponentIds = uniq(toMoveComponentIds);
|
||||
toDeleteComponentIds = uniq(toDeleteComponentIds);
|
||||
|
||||
const root = services.appModelManager.appModel
|
||||
.getComponentById(componentId as ComponentId)!
|
||||
.clone();
|
||||
const newModuleContainerId = `${componentId}__module`;
|
||||
const newModuleId = `${componentId}Module`;
|
||||
// remove root slot
|
||||
root.removeSlotTrait();
|
||||
|
||||
// covert in-module components to schema
|
||||
const eventSpec: string[] = [];
|
||||
const moduleComponentsSchema = root?.allComponents.map(c => {
|
||||
const eventTrait = c.traits.find(t => t.type === 'core/v1/event');
|
||||
// conver in-module components' event handlers
|
||||
if (eventTrait) {
|
||||
const cache: Record<string, boolean> = {};
|
||||
const handlers: Static<typeof EventHandlerSpec>[] = [];
|
||||
eventTrait?.rawProperties.handlers.forEach(
|
||||
(h: Static<typeof EventHandlerSpec>) => {
|
||||
const newEventName = `${c.id}${h.type}`;
|
||||
const hasRelation = methodRelations.find(r => {
|
||||
return (
|
||||
r.source === c.id && r.event === h.type && r.target === h.componentId
|
||||
);
|
||||
});
|
||||
if (hasRelation) {
|
||||
// if component has another handler emit the same event, don't emit it again
|
||||
if (cache[newEventName]) {
|
||||
return;
|
||||
}
|
||||
// emit new $module event
|
||||
cache[newEventName] = true;
|
||||
eventSpec.push(newEventName);
|
||||
handlers.push({
|
||||
type: h.type,
|
||||
componentId: GLOBAL_MODULE_ID,
|
||||
method: {
|
||||
name: newEventName,
|
||||
parameters: {
|
||||
moduleId: '{{$moduleId}}',
|
||||
},
|
||||
},
|
||||
disabled: false,
|
||||
wait: { type: 'delay', time: 0 },
|
||||
});
|
||||
} else {
|
||||
handlers.push(h);
|
||||
}
|
||||
}
|
||||
);
|
||||
eventTrait.updateProperty('handlers', handlers);
|
||||
}
|
||||
return c.toSchema();
|
||||
});
|
||||
|
||||
// add moved and duplicated components
|
||||
if (toMoveComponentIds.length) {
|
||||
toMoveComponentIds.forEach(id => {
|
||||
const comp = services.appModelManager.appModel.getComponentById(
|
||||
id as ComponentId
|
||||
)!;
|
||||
moduleComponentsSchema.push(comp.toSchema());
|
||||
});
|
||||
}
|
||||
|
||||
// generate event handlers for module container
|
||||
const moduleHandlers = methodRelations.map(r => {
|
||||
const { handler } = r;
|
||||
return {
|
||||
...handler,
|
||||
type: `${r.source}${r.event}`,
|
||||
};
|
||||
});
|
||||
|
||||
// generate StateMap
|
||||
const stateMap: Record<string, string> = {};
|
||||
const outsideComponentNewProperties: Record<string, any> = {};
|
||||
type TraitNewProperties = {
|
||||
componentId: string;
|
||||
traitIndex: number;
|
||||
properties: Record<string, any>;
|
||||
};
|
||||
const outsideTraitNewProperties: TraitNewProperties[] = [];
|
||||
outsideExpRelationsValueRef.current.forEach(r => {
|
||||
if (r.stateName) {
|
||||
const origin = `${r.relyOn}.${r.valuePath}`;
|
||||
stateMap[r.stateName] = origin;
|
||||
// replace ref with new state name in expressions
|
||||
const newExp = r.exp.replaceAll(origin, `${newModuleId}.${r.stateName}`);
|
||||
const c = services.appModelManager.appModel.getComponentById(
|
||||
r.componentId as ComponentId
|
||||
)!;
|
||||
const fieldKey = r.key.startsWith('.') ? r.key.slice(1) : r.key;
|
||||
if (r.traitType) {
|
||||
c.traits.forEach((t, i) => {
|
||||
const newProperties = set(t.properties.rawValue, fieldKey, newExp);
|
||||
if (t.type === r.traitType) {
|
||||
outsideTraitNewProperties.push({
|
||||
componentId: r.componentId,
|
||||
traitIndex: i,
|
||||
properties: newProperties,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const fieldKey = r.key.startsWith('.') ? r.key.slice(1) : r.key;
|
||||
const newProperties = set(c.properties.rawValue, fieldKey, newExp);
|
||||
outsideComponentNewProperties[r.componentId] = newProperties;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
exampleProperties,
|
||||
moduleContainerProperties,
|
||||
eventSpec,
|
||||
toMoveComponentIds,
|
||||
toDeleteComponentIds,
|
||||
methodRelations,
|
||||
moduleComponentsSchema,
|
||||
moduleHandlers,
|
||||
stateMap,
|
||||
newModuleContainerId,
|
||||
newModuleId,
|
||||
outsideComponentNewProperties,
|
||||
outsideTraitNewProperties,
|
||||
moduleRootId: componentId,
|
||||
};
|
||||
}, [
|
||||
componentId,
|
||||
insideExpRelations,
|
||||
methodRelations,
|
||||
services.appModelManager.appModel,
|
||||
services.stateManager.store,
|
||||
]);
|
||||
|
||||
const onExtract = () => {
|
||||
if (!genModuleResult || !moduleFormValueRef.current) return;
|
||||
services.editorStore.appStorage.createModule({
|
||||
components: genModuleResult.moduleComponentsSchema,
|
||||
propertySpec: moduleFormValueRef.current.properties,
|
||||
exampleProperties: genModuleResult.exampleProperties,
|
||||
events: genModuleResult.eventSpec,
|
||||
moduleVersion: moduleFormValueRef.current.version,
|
||||
moduleName: moduleFormValueRef.current.name,
|
||||
stateMap: genModuleResult.stateMap,
|
||||
});
|
||||
|
||||
services.eventBus.send(
|
||||
'operation',
|
||||
genOperation(services.registry, 'extractModule', {
|
||||
moduleContainerId: genModuleResult.newModuleContainerId,
|
||||
moduleContainerProperties: genModuleResult.moduleContainerProperties,
|
||||
moduleId: genModuleResult.newModuleId,
|
||||
moduleRootId: genModuleResult.moduleRootId,
|
||||
moduleType: `${moduleFormValueRef.current.version}/${moduleFormValueRef.current.name}`,
|
||||
moduleHandlers: genModuleResult.moduleHandlers,
|
||||
outsideComponentNewProperties: genModuleResult.outsideComponentNewProperties,
|
||||
outsideTraitNewProperties: genModuleResult.outsideTraitNewProperties,
|
||||
toDeleteComponentIds: genModuleResult.toDeleteComponentIds,
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// generate module spec for preview
|
||||
useEffect(() => {
|
||||
if (activeIndex === 3) {
|
||||
const result = genModule();
|
||||
serGenModuleResult(result);
|
||||
const moduleFormData = {
|
||||
name: componentId,
|
||||
version: 'custom/v1',
|
||||
stateMap: result.stateMap,
|
||||
properties: json2JsonSchema(result.exampleProperties),
|
||||
events: result.eventSpec,
|
||||
exampleProperties: result.exampleProperties,
|
||||
};
|
||||
setModuleFormInitData(moduleFormData);
|
||||
moduleFormValueRef.current = moduleFormData;
|
||||
}
|
||||
}, [activeIndex, componentId, genModule]);
|
||||
|
||||
return (
|
||||
<HStack spacing="12" height="full" alignItems="start">
|
||||
<ExtractModuleStep activeIndex={activeIndex} />
|
||||
<VStack height="full" width="full" alignItems="end">
|
||||
<Box width="full" overflow="auto">
|
||||
<Box width="full" display={activeIndex === 0 ? 'block' : 'none'}>
|
||||
<ExtractModulePropertyForm
|
||||
insideExpRelations={insideExpRelations}
|
||||
onChange={val => (refTreatmentMap.current = val)}
|
||||
services={services}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="full" display={activeIndex === 1 ? 'block' : 'none'}>
|
||||
<ExtractModuleStateForm
|
||||
outsideExpRelations={outsideExpRelations}
|
||||
onChange={v => {
|
||||
outsideExpRelationsValueRef.current = v;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="full" display={activeIndex === 2 ? 'block' : 'none'}>
|
||||
<ExtractModuleEventForm
|
||||
methodRelations={methodRelations}
|
||||
services={services}
|
||||
/>
|
||||
</Box>
|
||||
<VStack width="full" display={activeIndex === 3 ? 'block' : 'none'}>
|
||||
<Heading size="md">Preview Module Spec</Heading>
|
||||
<Text>
|
||||
{`The Spec has generated automatically, you don't need to change anything except version and name.`}
|
||||
</Text>
|
||||
{activeIndex === 3 && moduleFormInitData ? (
|
||||
<ModuleMetaDataForm
|
||||
services={services}
|
||||
initData={moduleFormInitData}
|
||||
onSubmit={value => (moduleFormValueRef.current = value)}
|
||||
/>
|
||||
) : undefined}
|
||||
</VStack>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<ButtonGroup flex="0 0">
|
||||
{activeIndex > 0 ? (
|
||||
<Button variant="outline" onClick={() => setActiveIndex(v => v - 1)}>
|
||||
Prev
|
||||
</Button>
|
||||
) : undefined}
|
||||
{activeIndex < 3 ? (
|
||||
<Button colorScheme="blue" onClick={() => setActiveIndex(v => v + 1)}>
|
||||
Next
|
||||
</Button>
|
||||
) : undefined}
|
||||
{activeIndex === 3 ? (
|
||||
<Button colorScheme="blue" onClick={onExtract}>
|
||||
Extract
|
||||
</Button>
|
||||
) : undefined}
|
||||
</ButtonGroup>
|
||||
</VStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export const Placeholder: React.FC<{ text: string }> = ({ text }) => {
|
||||
return (
|
||||
<Text
|
||||
width="full"
|
||||
padding="12px"
|
||||
background="gray.100"
|
||||
color="gray.500"
|
||||
borderRadius="4px"
|
||||
textAlign="center"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './ExtractModuleModal';
|
43
packages/editor/src/components/ExtractModuleModal/type.ts
Normal file
43
packages/editor/src/components/ExtractModuleModal/type.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import { EventHandlerSpec } from '@sunmao-ui/shared';
|
||||
|
||||
// out-module components' expression rely on in-module component
|
||||
export type OutsideExpRelation = {
|
||||
componentId: string;
|
||||
traitType?: string;
|
||||
exp: string;
|
||||
key: string;
|
||||
valuePath: string;
|
||||
relyOn: string;
|
||||
};
|
||||
|
||||
export type OutsideExpRelationWithState = OutsideExpRelation & {
|
||||
stateName: string;
|
||||
};
|
||||
|
||||
// in-module components rely on out-module components
|
||||
export type InsideExpRelation = {
|
||||
source: string;
|
||||
traitType?: string;
|
||||
componentId: string;
|
||||
exp: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export enum RefTreatment {
|
||||
'keep' = 'keep',
|
||||
'move' = 'move',
|
||||
'duplicate' = 'duplicate',
|
||||
'ignore' = 'ignore',
|
||||
}
|
||||
|
||||
export type RefTreatmentMap = Record<string, RefTreatment>;
|
||||
|
||||
// in-module components call out-module components' method
|
||||
export type InsideMethodRelation = {
|
||||
handler: Static<typeof EventHandlerSpec>;
|
||||
source: string;
|
||||
target: string;
|
||||
event: string;
|
||||
method: string;
|
||||
};
|
110
packages/editor/src/components/ExtractModuleModal/utils.ts
Normal file
110
packages/editor/src/components/ExtractModuleModal/utils.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import {
|
||||
CoreTraitName,
|
||||
CORE_VERSION,
|
||||
EventHandlerSpec,
|
||||
ExpressionKeywords,
|
||||
} from '@sunmao-ui/shared';
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import { IComponentModel, IFieldModel, ComponentId } from '../../AppModel/IAppModel';
|
||||
import { OutsideExpRelation, InsideExpRelation, InsideMethodRelation } from './type';
|
||||
|
||||
export 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)) {
|
||||
// component is in module, ignore.
|
||||
return;
|
||||
}
|
||||
const handler = (field: IFieldModel, key: string, traitType?: string) => {
|
||||
if (field.isDynamic) {
|
||||
const relyRefs = Object.keys(field.refComponentInfos).filter(refId =>
|
||||
ids.includes(refId as ComponentId)
|
||||
);
|
||||
relyRefs.forEach(refId => {
|
||||
res.push({
|
||||
componentId: c.id,
|
||||
traitType,
|
||||
exp: field.getValue() as string,
|
||||
key,
|
||||
valuePath:
|
||||
field.refComponentInfos[refId as ComponentId].refProperties.slice(-1)[0],
|
||||
relyOn: refId,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
// traverse all the expressions of all outisde components and traits
|
||||
c.properties.traverse((field, key) => {
|
||||
handler(field, key);
|
||||
c.traits.forEach(t => {
|
||||
t.properties.traverse((field, key) => {
|
||||
handler(field, key, t.type);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getInsideRelations(
|
||||
component: IComponentModel,
|
||||
moduleComponents: IComponentModel[]
|
||||
) {
|
||||
const expressionRelations: InsideExpRelation[] = [];
|
||||
const methodRelations: InsideMethodRelation[] = [];
|
||||
const ids = moduleComponents.map(c => c.id) as string[];
|
||||
|
||||
const handler = (field: IFieldModel, key: string, traitType?: string) => {
|
||||
if (field.isDynamic) {
|
||||
const usedIds = Object.keys(field.refComponentInfos);
|
||||
usedIds.forEach(usedId => {
|
||||
// ignore global vraiable and sunmao keywords
|
||||
if (
|
||||
!ids.includes(usedId) &&
|
||||
!(usedId in window) &&
|
||||
!ExpressionKeywords.includes(usedId)
|
||||
) {
|
||||
expressionRelations.push({
|
||||
traitType: traitType,
|
||||
source: component.id,
|
||||
componentId: usedId,
|
||||
exp: field.rawValue,
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
component.properties.traverse((field, key) => {
|
||||
handler(field, key);
|
||||
});
|
||||
|
||||
component.traits.forEach(t => {
|
||||
t.properties.traverse((field, key) => {
|
||||
handler(field, key, t.type);
|
||||
});
|
||||
|
||||
// check event traits, see if component call outside methods
|
||||
if (t.type === `${CORE_VERSION}/${CoreTraitName.Event}`) {
|
||||
t.rawProperties.handlers.forEach((h: Static<typeof EventHandlerSpec>) => {
|
||||
if (!ids.includes(h.componentId)) {
|
||||
methodRelations.push({
|
||||
source: component.id,
|
||||
event: h.type,
|
||||
target: h.componentId,
|
||||
method: h.method.name,
|
||||
handler: h,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { expressionRelations, methodRelations };
|
||||
}
|
@ -20,6 +20,7 @@ import { ComponentId } from '../../AppModel/IAppModel';
|
||||
import { RootId } from '../../constants';
|
||||
import { RelationshipModal } from '../RelationshipModal';
|
||||
import { ExplorerMenuTabs } from '../../constants/enum';
|
||||
import { ExtractModuleModal } from '../ExtractModuleModal';
|
||||
|
||||
const IndextWidth = 24;
|
||||
|
||||
@ -52,6 +53,7 @@ const ComponentNodeImpl = (props: Props) => {
|
||||
} = props;
|
||||
const { registry, eventBus, appModelManager, editorStore } = services;
|
||||
const [isShowRelationshipModal, setIsShowRelationshipModal] = useState(false);
|
||||
const [isShowExtractModuleModal, setIsShowExtractModuleModal] = useState(false);
|
||||
const slots = Object.keys(registry.getComponentByType(component.type).spec.slots);
|
||||
const paddingLeft = depth * IndextWidth;
|
||||
|
||||
@ -101,6 +103,10 @@ const ComponentNodeImpl = (props: Props) => {
|
||||
},
|
||||
[component.id, editorStore]
|
||||
);
|
||||
const onClickExtractToModule = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsShowExtractModuleModal(true);
|
||||
}, []);
|
||||
|
||||
const onClickItem = useCallback(() => {
|
||||
onSelectComponent(component.id);
|
||||
@ -183,6 +189,9 @@ const ComponentNodeImpl = (props: Props) => {
|
||||
<MenuItem icon={<ViewIcon />} onClick={onClickShowState}>
|
||||
Show State
|
||||
</MenuItem>
|
||||
<MenuItem icon={<ViewIcon />} onClick={onClickExtractToModule}>
|
||||
Extract to Module
|
||||
</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} color="red.500" onClick={onClickRemove}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
@ -198,6 +207,14 @@ const ComponentNodeImpl = (props: Props) => {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const extractModuleModal = isShowExtractModuleModal ? (
|
||||
<ExtractModuleModal
|
||||
componentId={component.id}
|
||||
services={services}
|
||||
onClose={() => setIsShowExtractModuleModal(false)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<VStack
|
||||
key={component.id}
|
||||
@ -255,6 +272,7 @@ const ComponentNodeImpl = (props: Props) => {
|
||||
</DropComponentWrapper>
|
||||
{emptyChildrenSlotsPlaceholder}
|
||||
{relationshipViewModal}
|
||||
{extractModuleModal}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,112 @@
|
||||
import { AppModel } from '../../../AppModel/AppModel';
|
||||
import {
|
||||
CreateComponentLeafOperation,
|
||||
ModifyComponentPropertiesLeafOperation,
|
||||
ModifyTraitPropertiesLeafOperation,
|
||||
} from '../../leaf';
|
||||
import { BaseBranchOperation } from '../../type';
|
||||
import { EventHandlerSpec } from '@sunmao-ui/shared';
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import { MoveComponentBranchOperation, RemoveComponentBranchOperation } from '..';
|
||||
import { ComponentId, SlotName } from '../../../AppModel/IAppModel';
|
||||
|
||||
type TraitNewProperties = {
|
||||
componentId: string;
|
||||
traitIndex: number;
|
||||
properties: Record<string, any>;
|
||||
};
|
||||
export type ExtractModuleBranchOperationContext = {
|
||||
moduleContainerId: string;
|
||||
moduleContainerProperties: Record<string, any>;
|
||||
moduleId: string;
|
||||
moduleRootId: string;
|
||||
moduleType: string;
|
||||
moduleHandlers: Static<typeof EventHandlerSpec>[];
|
||||
outsideComponentNewProperties: Record<string, any>;
|
||||
outsideTraitNewProperties: TraitNewProperties[];
|
||||
toDeleteComponentIds: string[];
|
||||
};
|
||||
|
||||
export class ExtractModuleBranchOperation extends BaseBranchOperation<ExtractModuleBranchOperationContext> {
|
||||
do(prev: AppModel): AppModel {
|
||||
const root = prev.getComponentById(this.context.moduleRootId as ComponentId);
|
||||
if (!root) {
|
||||
console.warn('component not found');
|
||||
return prev;
|
||||
}
|
||||
// create module container component
|
||||
this.operationStack.insert(
|
||||
new CreateComponentLeafOperation(this.registry, {
|
||||
componentId: this.context.moduleContainerId,
|
||||
componentType: `core/v1/moduleContainer`,
|
||||
})
|
||||
);
|
||||
|
||||
// add properties to module container component
|
||||
this.operationStack.insert(
|
||||
new ModifyComponentPropertiesLeafOperation(this.registry, {
|
||||
componentId: this.context.moduleContainerId,
|
||||
properties: {
|
||||
id: this.context.moduleId,
|
||||
type: this.context.moduleType,
|
||||
properties: this.context.moduleContainerProperties,
|
||||
handlers: this.context.moduleHandlers,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// move module container to the position of root
|
||||
this.operationStack.insert(
|
||||
new MoveComponentBranchOperation(this.registry, {
|
||||
fromId: this.context.moduleContainerId,
|
||||
toId: root.parentId as ComponentId,
|
||||
slot: root.parentSlot as SlotName,
|
||||
targetId: root.id,
|
||||
direction: 'next',
|
||||
})
|
||||
);
|
||||
|
||||
// update the properties of outside components
|
||||
for (const id in this.context.outsideComponentNewProperties) {
|
||||
this.operationStack.insert(
|
||||
new ModifyComponentPropertiesLeafOperation(this.registry, {
|
||||
componentId: id,
|
||||
properties: this.context.outsideComponentNewProperties[id],
|
||||
})
|
||||
);
|
||||
}
|
||||
// update the properties of outside components' trait
|
||||
this.context.outsideTraitNewProperties.forEach(
|
||||
({ componentId, traitIndex, properties }) => {
|
||||
this.operationStack.insert(
|
||||
new ModifyTraitPropertiesLeafOperation(this.registry, {
|
||||
componentId,
|
||||
traitIndex,
|
||||
properties,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// remove the module root component
|
||||
this.operationStack.insert(
|
||||
new RemoveComponentBranchOperation(this.registry, {
|
||||
componentId: this.context.moduleRootId,
|
||||
})
|
||||
);
|
||||
|
||||
// remove other components which are moved in to module
|
||||
this.context.toDeleteComponentIds.forEach(id => {
|
||||
this.operationStack.insert(
|
||||
new RemoveComponentBranchOperation(this.registry, {
|
||||
componentId: id,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return this.operationStack.reduce((prev, node) => {
|
||||
prev = node.do(prev);
|
||||
return prev;
|
||||
}, prev);
|
||||
}
|
||||
}
|
@ -2,4 +2,5 @@ export * from './component/createComponentBranchOperation';
|
||||
export * from './component/modifyComponentIdBranchOperation';
|
||||
export * from './component/removeComponentBranchOperation';
|
||||
export * from './component/moveComponentBranchOperation';
|
||||
export * from './component/extractModuleBranchOperation';
|
||||
export * from './datasource/createDataSourceBranchOperation';
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
RemoveComponentBranchOperationContext,
|
||||
MoveComponentBranchOperation,
|
||||
MoveComponentBranchOperationContext,
|
||||
ExtractModuleBranchOperation,
|
||||
ExtractModuleBranchOperationContext,
|
||||
CreateDataSourceBranchOperation,
|
||||
CreateDataSourceBranchOperationContext,
|
||||
} from './branch';
|
||||
@ -35,7 +37,7 @@ export const OperationConstructors: Record<
|
||||
> = {
|
||||
createComponent: CreateComponentBranchOperation,
|
||||
removeComponent: RemoveComponentBranchOperation,
|
||||
modifyComponentProperty: ModifyComponentPropertiesLeafOperation,
|
||||
modifyComponentProperties: ModifyComponentPropertiesLeafOperation,
|
||||
modifyComponentId: ModifyComponentIdBranchOperation,
|
||||
adjustComponentOrder: AdjustComponentOrderLeafOperation,
|
||||
createTrait: CreateTraitLeafOperation,
|
||||
@ -44,6 +46,7 @@ export const OperationConstructors: Record<
|
||||
replaceApp: ReplaceAppLeafOperation,
|
||||
pasteComponent: PasteComponentLeafOperation,
|
||||
moveComponent: MoveComponentBranchOperation,
|
||||
extractModule: ExtractModuleBranchOperation,
|
||||
createDataSource: CreateDataSourceBranchOperation,
|
||||
};
|
||||
|
||||
@ -64,7 +67,7 @@ export type OperationConfigMaps = {
|
||||
RemoveComponentBranchOperation,
|
||||
RemoveComponentBranchOperationContext
|
||||
>;
|
||||
modifyComponentProperty: OperationConfigMap<
|
||||
modifyComponentProperties: OperationConfigMap<
|
||||
ModifyComponentPropertiesLeafOperation,
|
||||
ModifyComponentPropertiesLeafOperationContext
|
||||
>;
|
||||
@ -98,6 +101,10 @@ export type OperationConfigMaps = {
|
||||
MoveComponentBranchOperation,
|
||||
MoveComponentBranchOperationContext
|
||||
>;
|
||||
extractModule: OperationConfigMap<
|
||||
ExtractModuleBranchOperation,
|
||||
ExtractModuleBranchOperationContext
|
||||
>;
|
||||
createDataSource: OperationConfigMap<
|
||||
CreateDataSourceBranchOperation,
|
||||
CreateDataSourceBranchOperationContext
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
import { observable, makeObservable, action, toJS } from 'mobx';
|
||||
import { Application, ComponentSchema, Module, RuntimeModule } from '@sunmao-ui/core';
|
||||
import {
|
||||
Application,
|
||||
ComponentSchema,
|
||||
Module,
|
||||
parseVersion,
|
||||
RuntimeModule,
|
||||
} from '@sunmao-ui/core';
|
||||
import { produce } from 'immer';
|
||||
import { DefaultNewModule, EmptyAppSchema } from '../constants';
|
||||
import { addModuleId, removeModuleId } from '../utils/addModuleId';
|
||||
@ -33,7 +39,24 @@ export class AppStorage {
|
||||
});
|
||||
}
|
||||
|
||||
createModule() {
|
||||
createModule(props: {
|
||||
components?: ComponentSchema[];
|
||||
propertySpec?: JSONSchema7;
|
||||
exampleProperties?: Record<string, any>;
|
||||
events?: string[];
|
||||
moduleVersion?: string;
|
||||
moduleName?: string;
|
||||
stateMap?: Record<string, string>;
|
||||
}): Module {
|
||||
const {
|
||||
components,
|
||||
propertySpec,
|
||||
exampleProperties,
|
||||
events,
|
||||
moduleVersion,
|
||||
moduleName,
|
||||
stateMap,
|
||||
} = props;
|
||||
let index = this.modules.length;
|
||||
|
||||
this.modules.forEach(module => {
|
||||
@ -45,19 +68,46 @@ export class AppStorage {
|
||||
const name = `myModule${index}`;
|
||||
const newModule: RuntimeModule = {
|
||||
...DefaultNewModule,
|
||||
parsedVersion: {
|
||||
...DefaultNewModule.parsedVersion,
|
||||
value: name,
|
||||
},
|
||||
version: moduleVersion || DefaultNewModule.version,
|
||||
parsedVersion: moduleVersion
|
||||
? parseVersion(moduleVersion)
|
||||
: DefaultNewModule.parsedVersion,
|
||||
metadata: {
|
||||
...DefaultNewModule.metadata,
|
||||
name,
|
||||
name: moduleName || name,
|
||||
},
|
||||
};
|
||||
|
||||
if (components) {
|
||||
newModule.impl = components;
|
||||
}
|
||||
|
||||
if (propertySpec) {
|
||||
newModule.spec.properties = propertySpec;
|
||||
}
|
||||
if (exampleProperties) {
|
||||
for (const key in exampleProperties) {
|
||||
const value = exampleProperties[key];
|
||||
if (typeof value === 'string') {
|
||||
newModule.metadata.exampleProperties![key] = value;
|
||||
} else {
|
||||
// save value as expression
|
||||
newModule.metadata.exampleProperties![key] = `{{${JSON.stringify(value)}}}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (events) {
|
||||
newModule.spec.events = events;
|
||||
}
|
||||
if (stateMap) {
|
||||
newModule.spec.stateMap = stateMap;
|
||||
}
|
||||
|
||||
this.setModules([...this.modules, newModule]);
|
||||
this.setRawModules(this.modules.map(addModuleId));
|
||||
const rawModules = this.modules.map(addModuleId);
|
||||
this.setRawModules(rawModules);
|
||||
this.saveModules();
|
||||
return rawModules[rawModules.length - 1];
|
||||
}
|
||||
|
||||
removeModule(v: string, n: string) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { action, makeAutoObservable, observable, reaction, toJS } from 'mobx';
|
||||
import { ComponentSchema, createModule } from '@sunmao-ui/core';
|
||||
import { RegistryInterface, StateManagerInterface } from '@sunmao-ui/runtime';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { EventBusType } from './eventBus';
|
||||
import { AppStorage } from './AppStorage';
|
||||
import type { SchemaValidator, ValidateErrorResult } from '../validator';
|
||||
import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { AppModelManager } from '../operations/AppModelManager';
|
||||
import type { Metadata } from '@sunmao-ui/core';
|
||||
|
||||
@ -34,6 +34,7 @@ export class EditorStore {
|
||||
currentEditingTarget: EditingTarget = {
|
||||
kind: 'app',
|
||||
version: '',
|
||||
|
||||
name: '',
|
||||
};
|
||||
|
||||
@ -115,6 +116,18 @@ export class EditorStore {
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => this.rawModules,
|
||||
() => {
|
||||
// Remove old modules and re-register all modules,
|
||||
this.registry.unregisterAllModules();
|
||||
this.rawModules.forEach(m => {
|
||||
const modules = createModule(m);
|
||||
this.registry.registerModule(modules, true);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => this.components,
|
||||
() => {
|
||||
|
@ -21,6 +21,7 @@ type Props = Static<typeof ModuleRenderSpec> & {
|
||||
evalScope?: Record<string, any>;
|
||||
services: UIServices;
|
||||
app: RuntimeApplication;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModuleRenderer = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
@ -37,7 +38,7 @@ const ModuleRendererContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
Props & { moduleSpec: ImplementedRuntimeModule }
|
||||
>((props, ref) => {
|
||||
const { moduleSpec, properties, handlers, evalScope, services, app } = props;
|
||||
const { moduleSpec, properties, handlers, evalScope, services, app, className } = props;
|
||||
const moduleId = services.stateManager.deepEval(props.id, {
|
||||
scopeObject: evalScope,
|
||||
}) as string | ExpressionError;
|
||||
@ -166,7 +167,7 @@ const ModuleRendererContent = React.forwardRef<
|
||||
}, [evaledModuleTemplate, services, app]);
|
||||
|
||||
return (
|
||||
<div className="module-container" ref={ref}>
|
||||
<div className={className} ref={ref}>
|
||||
{result}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import { implementRuntimeComponent } from '../../utils/buildKit';
|
||||
import { ModuleRenderSpec, CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
|
||||
import { ModuleRenderer } from '../_internal/ModuleRenderer';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export default implementRuntimeComponent({
|
||||
version: CORE_VERSION,
|
||||
@ -22,10 +23,10 @@ export default implementRuntimeComponent({
|
||||
state: {},
|
||||
methods: {},
|
||||
slots: {},
|
||||
styleSlots: [],
|
||||
styleSlots: ['content'],
|
||||
events: [],
|
||||
},
|
||||
})(({ id, type, properties, handlers, services, app, elementRef }) => {
|
||||
})(({ id, type, properties, handlers, services, app, elementRef, customStyle }) => {
|
||||
if (!type) {
|
||||
return <span ref={elementRef}>Please choose a module to render.</span>;
|
||||
}
|
||||
@ -36,6 +37,9 @@ export default implementRuntimeComponent({
|
||||
return (
|
||||
<ModuleRenderer
|
||||
id={id}
|
||||
className={css`
|
||||
${customStyle?.content}
|
||||
`}
|
||||
type={type}
|
||||
properties={properties}
|
||||
handlers={handlers}
|
||||
|
@ -18,7 +18,7 @@ dayjs.locale('zh-cn');
|
||||
type EvalOptions = {
|
||||
scopeObject?: Record<string, any>;
|
||||
overrideScope?: boolean;
|
||||
fallbackWhenError?: (exp: string) => any;
|
||||
fallbackWhenError?: (exp: string, err: Error) => any;
|
||||
// when ignoreEvalError is true, the eval process will continue after error happens in nests expression.
|
||||
ignoreEvalError?: boolean;
|
||||
slotKey?: string;
|
||||
@ -128,7 +128,9 @@ export class StateManager {
|
||||
consoleError(ConsoleType.Expression, raw, expressionError.message);
|
||||
}
|
||||
|
||||
return fallbackWhenError ? fallbackWhenError(raw) : expressionError;
|
||||
return fallbackWhenError
|
||||
? fallbackWhenError(raw, expressionError)
|
||||
: expressionError;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -169,18 +171,10 @@ export class StateManager {
|
||||
options: EvalOptions = {}
|
||||
): EvaledResult<T> {
|
||||
const store = this.slotStore;
|
||||
const redirector = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return options.slotKey ? store[options.slotKey][prop] : undefined;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
options.scopeObject = {
|
||||
...options.scopeObject,
|
||||
$slot: redirector,
|
||||
$slot: options.slotKey ? store[options.slotKey] : undefined,
|
||||
};
|
||||
// just eval
|
||||
if (typeof value !== 'string') {
|
||||
@ -208,17 +202,9 @@ export class StateManager {
|
||||
: PropsAfterEvaled<Exclude<T, string>>;
|
||||
|
||||
const store = this.slotStore;
|
||||
const redirector = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return options.slotKey ? store[options.slotKey][prop] : undefined;
|
||||
},
|
||||
}
|
||||
);
|
||||
options.scopeObject = {
|
||||
...options.scopeObject,
|
||||
$slot: redirector,
|
||||
$slot: options.slotKey ? store[options.slotKey] : undefined,
|
||||
};
|
||||
// watch change
|
||||
if (value && typeof value === 'object') {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { debounce, throttle, delay } from 'lodash';
|
||||
import { EventCallBackHandlerSpec } from '@sunmao-ui/shared';
|
||||
import { EventCallBackHandlerSpec, MODULE_ID_EXP } from '@sunmao-ui/shared';
|
||||
import { type PropsBeforeEvaled } from '@sunmao-ui/core';
|
||||
import { UIServices } from '../types';
|
||||
|
||||
@ -18,6 +18,10 @@ export const runEventHandler = (
|
||||
// Eval before sending event to assure the handler object is evaled from the latest state.
|
||||
const evalOptions = {
|
||||
slotKey,
|
||||
// keep MODULE_ID_EXP when error
|
||||
fallbackWhenError(exp: string, err: Error) {
|
||||
return exp === MODULE_ID_EXP ? exp : err;
|
||||
},
|
||||
};
|
||||
const evaledHandlers = stateManager.deepEval(rawHandlers, evalOptions) as Static<
|
||||
typeof EventCallBackHandlerSpec
|
||||
|
@ -7,6 +7,7 @@ export const LIST_ITEM_INDEX_EXP = '$i';
|
||||
export const SLOT_PROPS_EXP = '$slot';
|
||||
export const GLOBAL_UTIL_METHOD_ID = '$utils';
|
||||
export const GLOBAL_MODULE_ID = '$module';
|
||||
export const MODULE_ID_EXP = '{{$moduleId}}';
|
||||
|
||||
export const ExpressionKeywords = [
|
||||
LIST_ITEM_EXP,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { EventHandlerSpec } from './event';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { CORE_VERSION, CoreWidgetName } from '../constants/core';
|
||||
import { MODULE_ID_EXP } from '../constants';
|
||||
|
||||
export const ModuleRenderSpec = Type.Object(
|
||||
{
|
||||
@ -27,3 +28,7 @@ export const ModuleRenderSpec = Type.Object(
|
||||
widget: 'core/v1/module',
|
||||
}
|
||||
);
|
||||
|
||||
export const ModuleEventMethodSpec = Type.Object({
|
||||
moduleId: Type.Literal(MODULE_ID_EXP),
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user