Skip to content

Commit

Permalink
feat(editor): add extract module feature
Browse files Browse the repository at this point in the history
  • Loading branch information
tanbowensg committed Jan 13, 2023
1 parent 145df0d commit 2aa5095
Show file tree
Hide file tree
Showing 37 changed files with 1,497 additions and 142 deletions.
55 changes: 50 additions & 5 deletions packages/editor-sdk/src/components/Widgets/EventWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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[]>([]);

Expand Down Expand Up @@ -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]
Expand All @@ -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(
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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}
Expand Down
51 changes: 9 additions & 42 deletions packages/editor-sdk/src/components/Widgets/ExpressionWidget.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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>> {
Expand Down Expand Up @@ -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
Expand Down
37 changes: 3 additions & 34 deletions packages/editor-sdk/src/components/Widgets/ModuleWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 (
Expand Down
5 changes: 5 additions & 0 deletions packages/editor-sdk/src/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-sdk/src/types/operation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type OperationType =
| 'createComponent'
| 'removeComponent'
| 'modifyComponentProperty'
| 'modifyComponentProperties'
| 'modifyComponentId'
| 'adjustComponentOrder'
| 'createTrait'
Expand Down
1 change: 1 addition & 0 deletions packages/editor-sdk/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './widget';
export * from './validator';
export * from './type';
68 changes: 68 additions & 0 deletions packages/editor-sdk/src/utils/type.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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;
};
14 changes: 14 additions & 0 deletions packages/editor/src/AppModel/ComponentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/AppModel/IAppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
Loading

0 comments on commit 2aa5095

Please sign in to comment.