Merge branch 'lihao/preset-vision-test' into 'release/0.9.33'

Lihao/preset vision test

1. 新增 preset-vision 的部分单测;
2. 支持(恢复)修改 device 后的画布变化;
3. props-reducers 代码重构,收拢;
4. project.unload() bugfix;

See merge request !1039388
This commit is contained in:
高凯 2020-11-09 14:16:17 +08:00
commit e521e9c913
36 changed files with 2158 additions and 313 deletions

View File

@ -4,8 +4,9 @@
"private": true,
"description": "低代码引擎 DEMO",
"scripts": {
"cloud-build": "build-scripts build --config cloud-build.json",
"start": "build-scripts start"
"start": "build-scripts start",
"build": "build-scripts build",
"cloud-build": "build-scripts build --config cloud-build.json"
},
"config": {},
"dependencies": {

View File

@ -14,5 +14,6 @@ module.exports = {
'no-useless-constructor': 1,
'no-empty-function': 1,
'@typescript-eslint/member-ordering': 0,
'lines-between-class-members': 0
}
}

View File

@ -1,5 +1,6 @@
{
"plugins": [
"build-plugin-component",
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -10,7 +10,7 @@
],
"scripts": {
"build": "build-scripts build --skip-demo",
"test": "build-scripts --config build.test.json test"
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {

View File

@ -9,7 +9,7 @@ export interface Serialization<T = any> {
unserialize(data: T): NodeSchema;
}
let currentSerializion: Serialization<any> = {
let currentSerialization: Serialization<any> = {
serialize(data: NodeSchema): string {
return JSON.stringify(data);
},
@ -18,8 +18,8 @@ let currentSerializion: Serialization<any> = {
},
};
export function setSerialization(serializion: Serialization) {
currentSerializion = serializion;
export function setSerialization(serialization: Serialization) {
currentSerialization = serialization;
}
export class History {
@ -46,7 +46,7 @@ export class History {
return;
}
untracked(() => {
const log = currentSerializion.serialize(data);
const log = currentSerialization.serialize(data);
if (this.session.cursor === 0 && this.session.isActive()) {
// first log
this.session.log(log);
@ -98,7 +98,7 @@ export class History {
this.obx.sleep();
try {
this.redoer(currentSerializion.unserialize(hotData));
this.redoer(currentSerialization.unserialize(hotData));
this.emitter.emit('cursor', hotData);
} catch (e) {
//

View File

@ -101,7 +101,9 @@ export class Project {
if (this.documents.length < 1) {
return;
}
this.documents.forEach((doc) => doc.remove());
for (let i = this.documents.length - 1; i >= 0; i--) {
this.documents[i].remove();
}
}
removeDocument(doc: DocumentModel) {

View File

@ -1,5 +1,9 @@
import { IocContext } from 'power-di';
// 原本的 canBeKey 里判断函数的方法是 instanceof Function在某些 babel 编译 class 后的场景不满足该判断条件,此处暴力破解
IocContext.prototype.canBeKey = (obj: any) =>
typeof obj === 'function' || ['string', 'symbol'].includes(typeof obj);
export * from 'power-di';
export const globalContext = IocContext.DefaultInstance;

View File

@ -2,7 +2,7 @@ module.exports = {
extends: 'eslint-config-ali/typescript/react',
rules: {
'react/no-multi-comp': 1,
'no-unused-expressions': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,
@ -16,5 +16,6 @@ module.exports = {
'react/no-deprecated': 1,
'no-useless-escape': 1,
'brace-style': 1,
'@typescript-eslint/member-ordering': 0,
}
}

View File

@ -0,0 +1,19 @@
{
"plugins": [
[
"build-plugin-component",
{
"filename": "editor-preset-vision",
"library": "LowcodeEditor",
"libraryTarget": "umd",
"externals": {
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"rax": "var window.Rax"
}
}
],
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -0,0 +1,23 @@
const esModules = [
'@recore/obx-react',
// '@ali/lowcode-editor-core',
].join('|');
module.exports = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
],
};

View File

@ -11,7 +11,8 @@
],
"scripts": {
"build": "build-scripts build --skip-demo",
"cloud-build": "build-scripts build --skip-demo"
"cloud-build": "build-scripts build --skip-demo",
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {
@ -37,6 +38,7 @@
"react-dom": "^16.8.1"
},
"devDependencies": {
"@ali/lowcode-test-mate": "^1.0.0",
"@alib/build-scripts": "^0.1.18",
"@types/domready": "^1.0.0",
"@types/events": "^3.0.0",
@ -45,6 +47,7 @@
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0",
"build-plugin-react-app": "^1.1.2",
"prop-types": "^15.7.2",
"tsconfig-paths-webpack-plugin": "^3.2.0"
},
"publishConfig": {

View File

@ -64,15 +64,15 @@ export class Bus {
const bus = new Bus();
editor.on('hotkey.callback.call', (data) => {
editor?.on('hotkey.callback.call', (data) => {
bus.emit('ve.hotkey.callback.call', data);
});
editor.on('history.back', (data) => {
editor?.on('history.back', (data) => {
bus.emit('ve.history.back', data);
});
editor.on('history.forward', (data) => {
editor?.on('history.forward', (data) => {
bus.emit('ve.history.forward', data);
});

View File

@ -3,8 +3,6 @@ import { isPlainObject, hasOwnProperty, cloneDeep, isI18NObject, isUseI18NSetter
import { globalContext, Editor } from '@ali/lowcode-editor-core';
import { Designer, LiveEditing, TransformStage, Node, getConvertedExtraKey } from '@ali/lowcode-designer';
import Outline, { OutlineBackupPane, getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
import { toCss } from '@ali/vu-css-style';
import logger from '@ali/vu-logger';
import bus from './bus';
import { VE_EVENTS } from './base/const';
@ -13,7 +11,16 @@ import { Skeleton, SettingsPrimaryPane, registerDefaults } from '@ali/lowcode-ed
import { deepValueParser } from './deep-value-parser';
import { liveEditingRule, liveEditingSaveHander } from './vc-live-editing';
import { isVariable } from './utils';
import {
compatibleReducer,
compatiblePageReducer,
stylePropsReducer,
upgradePropsReducer,
filterReducer,
removeEmptyPropsReducer,
initNodeReducer,
liveLifecycleReducer,
} from './props-reducers';
export const editor = new Editor();
globalContext.register(editor, Editor);
@ -31,313 +38,31 @@ designer.project.onCurrentDocumentChange((doc) => {
bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY);
});
interface Variable {
type: 'variable';
variable: string;
value: any;
}
function upgradePropsReducer(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
if (isJSBlock(props)) {
if (props.value.componentName === 'Slot') {
return {
type: 'JSSlot',
title: (props.value.props as any)?.slotTitle,
name: (props.value.props as any)?.slotName,
value: props.value.children,
};
} else {
return props.value;
}
}
if (isVariable(props)) {
return {
type: 'JSExpression',
value: props.variable,
mock: props.value,
};
}
const newProps: any = {};
Object.keys(props).forEach((key) => {
if (/^__slot__/.test(key) && props[key] === true) {
return;
}
newProps[key] = upgradePropsReducer(props[key]);
});
return newProps;
}
// 升级 Props
designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade);
function getCurrentFieldIds() {
const fieldIds: any = [];
const nodesMap = designer?.currentDocument?.nodesMap || new Map();
nodesMap.forEach((curNode: any) => {
const fieldId = nodesMap?.get(curNode.id)?.getPropValue('fieldId');
if (fieldId) {
fieldIds.push(fieldId);
}
});
return fieldIds;
}
// 节点 props 初始化
designer.addPropsReducer((props, node) => {
// debugger;
// run initials
const newProps: any = {
...props,
};
if (newProps.fieldId) {
const fieldIds = getCurrentFieldIds();
designer.addPropsReducer(initNodeReducer, TransformStage.Init);
// 全局的关闭 uniqueIdChecker 信号,在 ve-utils 中实现
if (fieldIds.indexOf(props.fieldId) >= 0 && !(window as any).__disable_unique_id_checker__) {
newProps.fieldId = undefined;
}
}
const initials = node.componentMeta.getMetadata().experimental?.initials;
if (initials) {
const getRealValue = (propValue: any) => {
if (isVariable(propValue)) {
return propValue.value;
}
if (isJSExpression(propValue)) {
return propValue.mock;
}
return propValue;
};
initials.forEach((item) => {
// FIXME! this implements SettingTarget
try {
// FIXME! item.name could be 'xxx.xxx'
const ov = newProps[item.name];
const v = item.initial(node as any, getRealValue(ov));
if (ov === undefined && v !== undefined) {
newProps[item.name] = v;
}
// 兼容 props 中的属性为 i18n 类型,但是仅提供了一个字符串值,非变量绑定
if (isUseI18NSetter(node.componentMeta.prototype, item.name) &&
!isI18NObject(ov) &&
!isJSExpression(ov) &&
!isJSBlock(ov) &&
!isJSSlot(ov) &&
!isVariable(ov) &&
(isString(v) || isI18NObject(v))) {
newProps[item.name] = convertToI18NObject(v);
}
} catch (e) {
if (hasOwnProperty(props, item.name)) {
newProps[item.name] = props[item.name];
}
}
if (newProps[item.name] && !node.props.has(item.name)) {
node.props.add(newProps[item.name], item.name, false, { skipSetSlot: true });
}
});
}
return newProps;
}, TransformStage.Init);
designer.addPropsReducer(liveLifecycleReducer, TransformStage.Render);
designer.addPropsReducer((props: any, node: Node) => {
// live 模式下解析 lifeCycles
if (node.isRoot() && props && props.lifeCycles) {
if (editor.get('designMode') === 'live') {
const lifeCycleMap = {
didMount: 'componentDidMount',
willUnmount: 'componentWillUnMount',
};
const lifeCycles = props.lifeCycles;
Object.keys(lifeCycleMap).forEach(key => {
if (lifeCycles[key]) {
lifeCycles[lifeCycleMap[key]] = lifeCycles[key];
}
});
return props;
}
return {
...props,
lifeCycles: {},
};
}
return props;
}, TransformStage.Render);
function filterReducer(props: any, node: Node): any {
const filters = node.componentMeta.getMetadata().experimental?.filters;
if (filters && filters.length) {
const newProps = { ...props };
filters.forEach((item) => {
// FIXME! item.name could be 'xxx.xxx'
if (!hasOwnProperty(newProps, item.name)) {
return;
}
try {
if (item.filter(node.settingEntry.getProp(item.name), props[item.name]) === false) {
delete newProps[item.name];
}
} catch (e) {
console.warn(e);
logger.trace(e);
}
});
return newProps;
}
return props;
}
designer.addPropsReducer(filterReducer, TransformStage.Save);
designer.addPropsReducer(filterReducer, TransformStage.Render);
function compatiableReducer(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
// 为了能降级到老版本,建议在后期版本去掉以下代码
if (isJSSlot(props)) {
return {
type: 'JSBlock',
value: {
componentName: 'Slot',
children: props.value,
props: {
slotTitle: props.title,
slotName: props.name,
},
},
};
}
if (isJSExpression(props) && !props.events) {
return {
type: 'variable',
value: props.mock,
variable: props.value,
};
}
const newProps: any = {};
Object.entries<any>(props).forEach(([key, val]) => {
newProps[key] = compatiableReducer(val);
});
return newProps;
}
// FIXME: Dirty fix, will remove this reducer
designer.addPropsReducer(compatiableReducer, TransformStage.Save);
designer.addPropsReducer(compatibleReducer, TransformStage.Save);
// 兼容历史版本的 Page 组件
designer.addPropsReducer((props: any, node: Node) => {
const lifeCycleNames = ['didMount', 'willUnmount'];
if (node.isRoot()) {
lifeCycleNames.forEach((key) => {
if (props[key]) {
const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
lifeCycles[key] = props[key];
node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
}
});
}
return props;
}, TransformStage.Save);
// designer.addPropsReducer((props: any, node: Node) => {
// const lifeCycleNames = ['didMount', 'willUnmount'];
// const lifeCycleMap = {
// didMount: 'componentDidMount',
// willUnmount: 'componentWillUnMount',
// };
// if (node.componentName === 'Page') {
// debugger;
// lifeCycleNames.forEach(key => {
// if (props[key]) {
// const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
// lifeCycles[lifeCycleMap[key]] = props[key];
// node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
// } else if (node.props.getPropValue(getConvertedExtraKey('lifeCycles'))) {
// const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
// lifeCycles[lifeCycleMap[key]] = lifeCycles[key];
// node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
// }
// });
// }
// return props;
// }, TransformStage.Init);
designer.addPropsReducer(compatiblePageReducer, TransformStage.Save);
// 设计器组件样式处理
function stylePropsReducer(props: any, node: any) {
if (props && typeof props === 'object' && props.__style__) {
const cssId = `_style_pesudo_${ node.id.replace(/\$/g, '_')}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.__style__;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.pageStyle) {
const cssId = '_style_pesudo_engine-document';
const cssClass = 'engine-document';
const styleProp = props.pageStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.containerStyle) {
const cssId = `_style_pesudo_${ node.id}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.containerStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
return props;
}
function appendStyleNode(props: any, styleProp: any, cssClass: string, cssId: string) {
const doc = designer.currentDocument?.simulator?.contentDocument;
if (!doc) {
return;
}
const dom = doc.getElementById(cssId);
if (dom) {
dom.parentNode?.removeChild(dom);
}
if (typeof styleProp === 'object') {
styleProp = toCss(styleProp);
}
if (typeof styleProp === 'string') {
const s = doc.createElement('style');
props.className = cssClass;
s.setAttribute('type', 'text/css');
s.setAttribute('id', cssId);
doc.getElementsByTagName('head')[0].appendChild(s);
s.appendChild(doc.createTextNode(styleProp.replace(/(\d+)rpx/g, (a, b) => {
return `${b / 2}px`;
}).replace(/:root/g, `.${ cssClass}`)));
}
}
designer.addPropsReducer(stylePropsReducer, TransformStage.Render);
// 国际化 & Expression 渲染时处理
designer.addPropsReducer(deepValueParser, TransformStage.Render);
// 清除空的 props value
function removeEmptyProps(props: any, node: Node) {
if (node.isRoot() && props.dataSource) {
const online = cloneDeep(props.dataSource.online);
online.forEach((item: any) => {
const newParam: any = {};
if (Array.isArray(item?.options?.params)) {
item.options.params.forEach((element: any) => {
if (element.name) {
newParam[element.name] = element.value;
}
});
item.options.params = newParam;
}
});
props.dataSource.list = online;
}
return props;
}
// Init 的时候没有拿到 dataSource, 只能在 Render 和 Save 的时候都调用一次,理论上执行时机在 Init
// Render 和 Save 都要各调用一次,感觉也是有问题的,是不是应该在 Render 执行一次就行了?见上 filterReducer 也是一样的处理方式。
designer.addPropsReducer(removeEmptyProps, TransformStage.Render);
designer.addPropsReducer(removeEmptyProps, TransformStage.Save);
designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Render);
designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Save);
skeleton.add({
area: 'mainArea',

View File

@ -162,10 +162,10 @@ export {
Symbols,
};
const version = '6.0.0(LowcodeEngine 0.9.3)';
const version = '6.0.0 (LowcodeEngine 0.9.32)';
console.log(
`%c VisionEngine %c v${version} `,
'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060;font-weight:bold;',
'padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e;font-weight:bold;',
'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060; font-weight: bold;',
'padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e; font-weight: bold;',
);

View File

@ -64,7 +64,6 @@ const pages = Object.assign(project, {
item.methods = {};
}
});
console.log(pages, componentsTree);
project.load(
{
version: '1.0.0',

View File

@ -0,0 +1,51 @@
import { getConvertedExtraKey } from '@ali/lowcode-designer';
import {
isPlainObject,
} from '@ali/lowcode-utils';
import { isJSExpression, isJSSlot } from '@ali/lowcode-types';
export function compatibleReducer(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
// 为了能降级到老版本,建议在后期版本去掉以下代码
if (isJSSlot(props)) {
return {
type: 'JSBlock',
value: {
componentName: 'Slot',
children: props.value,
props: {
slotTitle: props.title,
slotName: props.name,
},
},
};
}
if (isJSExpression(props) && !props.events) {
return {
type: 'variable',
value: props.mock,
variable: props.value,
};
}
const newProps: any = {};
Object.entries<any>(props).forEach(([key, val]) => {
newProps[key] = compatibleReducer(val);
});
return newProps;
}
export function compatiblePageReducer(props: any, node: Node) {
const lifeCycleNames = ['didMount', 'willUnmount'];
if (node.isRoot()) {
lifeCycleNames.forEach(key => {
if (props[key]) {
const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
lifeCycles[key] = props[key];
node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
}
});
}
return props;
}

View File

@ -0,0 +1,25 @@
import logger from '@ali/vu-logger';
import { hasOwnProperty } from '@ali/lowcode-utils';
export function filterReducer(props: any, node: Node): any {
const filters = node.componentMeta.getMetadata().experimental?.filters;
if (filters && filters.length) {
const newProps = { ...props };
filters.forEach((item) => {
// FIXME! item.name could be 'xxx.xxx'
if (!hasOwnProperty(newProps, item.name)) {
return;
}
try {
if (item.filter(node.settingEntry.getProp(item.name), props[item.name]) === false) {
delete newProps[item.name];
}
} catch (e) {
console.warn(e);
logger.trace(e);
}
});
return newProps;
}
return props;
}

View File

@ -0,0 +1,7 @@
export * from './downgrade-schema-reducer';
export * from './filter-reducer';
export * from './init-node-reducer';
export * from './live-lifecycle-reducer';
export * from './remove-empty-prop-reducer';
export * from './style-reducer';
export * from './upgrade-reducer';

View File

@ -0,0 +1,68 @@
import {
hasOwnProperty,
isI18NObject,
isUseI18NSetter,
convertToI18NObject,
isString,
} from '@ali/lowcode-utils';
import { isJSExpression, isJSBlock, isJSSlot } from '@ali/lowcode-types';
import { isVariable, getCurrentFieldIds } from '../utils';
export function initNodeReducer(props, node) {
// debugger;
// run initials
const newProps: any = {
...props,
};
if (newProps.fieldId) {
const fieldIds = getCurrentFieldIds();
// 全局的关闭 uniqueIdChecker 信号,在 ve-utils 中实现
if (fieldIds.indexOf(props.fieldId) >= 0 && !(window as any).__disable_unique_id_checker__) {
newProps.fieldId = undefined;
}
}
const initials = node.componentMeta.getMetadata().experimental?.initials;
if (initials) {
const getRealValue = (propValue: any) => {
if (isVariable(propValue)) {
return propValue.value;
}
if (isJSExpression(propValue)) {
return propValue.mock;
}
return propValue;
};
initials.forEach(item => {
// FIXME! this implements SettingTarget
try {
// FIXME! item.name could be 'xxx.xxx'
const ov = newProps[item.name];
const v = item.initial(node as any, getRealValue(ov));
if (ov === undefined && v !== undefined) {
newProps[item.name] = v;
}
// 兼容 props 中的属性为 i18n 类型,但是仅提供了一个字符串值,非变量绑定
if (
isUseI18NSetter(node.componentMeta.prototype, item.name) &&
!isI18NObject(ov) &&
!isJSExpression(ov) &&
!isJSBlock(ov) &&
!isJSSlot(ov) &&
!isVariable(ov) &&
(isString(v) || isI18NObject(v))
) {
newProps[item.name] = convertToI18NObject(v);
}
} catch (e) {
if (hasOwnProperty(props, item.name)) {
newProps[item.name] = props[item.name];
}
}
if (newProps[item.name] && !node.props.has(item.name)) {
node.props.add(newProps[item.name], item.name, false, { skipSetSlot: true });
}
});
}
return newProps;
}

View File

@ -0,0 +1,27 @@
import { globalContext, Editor } from '@ali/lowcode-editor-core';
import { Node } from '@ali/lowcode-designer';
export function liveLifecycleReducer(props: any, node: Node) {
const editor = globalContext.get(Editor);
// live 模式下解析 lifeCycles
if (node.isRoot() && props && props.lifeCycles) {
if (editor.get('designMode') === 'live') {
const lifeCycleMap = {
didMount: 'componentDidMount',
willUnmount: 'componentWillUnMount',
};
const lifeCycles = props.lifeCycles;
Object.keys(lifeCycleMap).forEach(key => {
if (lifeCycles[key]) {
lifeCycles[lifeCycleMap[key]] = lifeCycles[key];
}
});
return props;
}
return {
...props,
lifeCycles: {},
};
}
return props;
}

View File

@ -0,0 +1,23 @@
import {
cloneDeep,
} from '@ali/lowcode-utils';
// 清除空的 props value
export function removeEmptyPropsReducer(props: any, node: Node) {
if (node.isRoot() && props.dataSource && Array.isArray(props.dataSource.online)) {
const online = cloneDeep(props.dataSource.online);
online.forEach((item: any) => {
const newParam: any = {};
if (Array.isArray(item?.options?.params)) {
item.options.params.forEach((element: any) => {
if (element.name) {
newParam[element.name] = element.value;
}
});
item.options.params = newParam;
}
});
props.dataSource.list = online;
}
return props;
}

View File

@ -0,0 +1,51 @@
import { globalContext, Editor } from '@ali/lowcode-editor-core';
import { toCss } from '@ali/vu-css-style';
export function stylePropsReducer(props: any, node: any) {
if (props && typeof props === 'object' && props.__style__) {
const cssId = `_style_pesudo_${ node.id.replace(/\$/g, '_')}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.__style__;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.pageStyle) {
const cssId = '_style_pesudo_engine-document';
const cssClass = 'engine-document';
const styleProp = props.pageStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.containerStyle) {
const cssId = `_style_pesudo_${ node.id}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.containerStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
return props;
}
function appendStyleNode(props: any, styleProp: any, cssClass: string, cssId: string) {
const editor = globalContext.get(Editor);
const designer = editor.get('designer');
const doc = designer.currentDocument?.simulator?.contentDocument;
if (!doc) {
return;
}
const dom = doc.getElementById(cssId);
if (dom) {
dom.parentNode?.removeChild(dom);
}
if (typeof styleProp === 'object') {
styleProp = toCss(styleProp);
}
if (typeof styleProp === 'string') {
const s = doc.createElement('style');
props.className = cssClass;
s.setAttribute('type', 'text/css');
s.setAttribute('id', cssId);
doc.getElementsByTagName('head')[0].appendChild(s);
s.appendChild(doc.createTextNode(styleProp.replace(/(\d+)rpx/g, (a, b) => {
return `${b / 2}px`;
}).replace(/:root/g, `.${ cssClass}`)));
}
}

View File

@ -0,0 +1,38 @@
import {
isPlainObject,
} from '@ali/lowcode-utils';
import { isJSBlock } from '@ali/lowcode-types';
import { isVariable } from '../utils';
export function upgradePropsReducer(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
if (isJSBlock(props)) {
if (props.value.componentName === 'Slot') {
return {
type: 'JSSlot',
title: (props.value.props as any)?.slotTitle,
name: (props.value.props as any)?.slotName,
value: props.value.children,
};
} else {
return props.value;
}
}
if (isVariable(props)) {
return {
type: 'JSExpression',
value: props.variable,
mock: props.value,
};
}
const newProps: any = {};
Object.keys(props).forEach((key) => {
if (/^__slot__/.test(key) && props[key] === true) {
return;
}
newProps[key] = upgradePropsReducer(props[key]);
});
return newProps;
}

View File

@ -1,3 +1,25 @@
export function isVariable(obj: any) {
import { globalContext, Editor } from '@ali/lowcode-editor-core';
interface Variable {
type: 'variable';
variable: string;
value: any;
}
export function isVariable(obj: any): obj is Variable {
return obj && obj.type === 'variable';
}
export function getCurrentFieldIds() {
const editor = globalContext.get(Editor);
const designer = editor.get('designer');
const fieldIds: any = [];
const nodesMap = designer?.currentDocument?.nodesMap || new Map();
nodesMap.forEach((curNode: any) => {
const fieldId = nodesMap?.get(curNode.id)?.getPropValue('fieldId');
if (fieldId) {
fieldIds.push(fieldId);
}
});
return fieldIds;
}

View File

@ -0,0 +1,71 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import divPrototypeConfig from '../fixtures/prototype/div-vision';
import divPrototypeMeta from '../fixtures/prototype/div-meta';
// import VisualEngine from '../../src';
import { designer } from '../../src/editor';
import Prototype from '../../src/bundle/prototype';
import { Editor } from '@ali/lowcode-editor-core';
// import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
describe('Prototype', () => {
it('构造函数 - OldPrototypeConfig', () => {
const proto = new Prototype(divPrototypeConfig);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe('http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md');
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - ComponentMetadata', () => {
const proto = new Prototype(divPrototypeMeta);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe('http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md');
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - ComponentMeta', () => {
const meta = designer.createComponentMeta(divPrototypeMeta);
const proto = new Prototype(meta);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe('http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md');
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - 静态函数 create', () => {
const proto = Prototype.create(divPrototypeConfig);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe('http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md');
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - lookup: true', () => {
const proto = Prototype.create(divPrototypeConfig);
const proto2 = Prototype.create(divPrototypeConfig, {}, true);
expect(proto).toBe(proto2);
});
});

View File

@ -0,0 +1,259 @@
export default {
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkh97h5kc',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [],
autoruns: [],
},
};

View File

@ -0,0 +1,175 @@
export default {
title: '容器',
componentName: 'Div',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
category: '布局',
isContainer: true,
configure: [
{
name: 'behavior',
title: '默认状态',
display: 'inline',
initialValue: 'NORMAL',
supportVariable: true,
setter: {
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
},
{
name: '__style__',
title: '样式设置',
display: 'accordion',
collapsed: false,
initialValue: {},
tip: {
url: 'https://lark.alipay.com/legao/help/design-tool-style',
content: '点击 ? 查看样式设置器用法指南',
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
title: '高级',
display: 'accordion',
items: [
{
name: 'fieldId',
title: '唯一标识',
display: 'block',
tip:
'组件的唯一标识符,不能够与其它组件重名,不能够为空,且只能够使用以字母开头的,下划线以及数字的组合。',
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'useFieldIdAsDomId',
title: '将唯一标识用作 DOM ID',
display: 'block',
tip:
'开启这个配置项后,会在当前组件的 HTML 元素上加入 id="当前组件的 fieldId",一般用于做 utils 的绑定,不常用',
initialValue: false,
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
name: 'customClassName',
title: '自定义样式类',
display: 'block',
supportVariable: true,
initialValue: '',
setter: {
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'events',
title: '动作设置',
tip: {
url: 'https://lark.alipay.com/legao/legao/events-call',
content: '点击 ? 查看如何设置组件的事件响应动作',
},
display: 'accordion',
initialValue: {
ignored: true,
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
name: 'onClick',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseEnter',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseLeave',
display: 'none',
initialValue: {
ignored: true,
},
},
],
},
],
};

View File

@ -0,0 +1,955 @@
export default {
componentName: 'Page',
id: 'node_k1ow3cb9',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kh05zf9c',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kh05zf9d {\n margin-bottom: 12px;\n}.card_kh05zf9e {\n margin-bottom: 12px;\n}.button_kh05zf9f {\n margin-right: 16px;\n width: 80px\n}.button_kh05zf9g {\n width: 80px;\n}.div_kh05zf9h {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
dataSource: {
offline: [],
globalConfig: {
fit: {
compiled: '',
source: '',
type: 'js',
error: {},
},
},
online: [],
sync: true,
list: [],
},
children: [
{
componentName: 'RootHeader',
id: 'node_k1ow3cba',
props: {},
condition: true,
children: [
{
componentName: 'PageHeader',
id: 'node_k1ow3cbd',
props: {
extraContent: '',
__slot__extraContent: false,
__slot__action: false,
title: '',
content: '',
__slot__logo: false,
__slot__crumb: false,
crumb: '',
tab: '',
logo: '',
action: '',
__slot__tab: false,
__style__: {},
__slot__content: false,
fieldId: 'pageHeader_k1ow3h1i',
subTitle: '',
},
condition: true,
},
],
},
{
componentName: 'RootContent',
id: 'node_k1ow3cbb',
props: {
contentBgColor: 'transparent',
contentPadding: '0',
contentMargin: '20',
},
condition: true,
children: [
{
componentName: 'Form',
id: 'node_k1ow3cbq',
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
condition: true,
children: [
{
componentName: 'Card',
id: 'node_k1ow3cbj',
props: {
__slot__title: false,
subTitle: {
use: 'zh_CN',
en_US: '',
zh_CN: '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
className: 'card_kh05zf9d',
title: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '基本信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1l',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbk',
props: {},
condition: true,
children: [
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cbw',
props: {
layout: '4:8',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h1v',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cbx',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjm',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cbz',
props: {
fieldName: 'name',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [
{
type: 'required',
},
],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1w',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '姓名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc1',
props: {
fieldName: 'englishName',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1y',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '英文名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc3',
props: {
fieldName: 'jobTitle',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h20',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '职位',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cby',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjn',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc2',
props: {
fieldName: 'nickName',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1z',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '花名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
{
componentName: 'SelectField',
id: 'node_k1ow3cc0',
props: {
fieldName: 'gender',
hasClear: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
mode: 'single',
showSearch: false,
autoWidth: true,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please select',
zh_CN: '请选择',
type: 'i18n',
},
hasBorder: true,
behavior: 'NORMAL',
value: '',
validation: [
{
type: 'required',
},
],
__style__: {},
fieldId: 'select_k1ow3h1x',
notFoundContent: {
use: 'zh_CN',
type: 'i18n',
},
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'SelectField',
zh_CN: '性别',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
wrapperColOffset: 0,
hasSelectAll: false,
hasArrow: true,
size: 'medium',
labelAlign: 'top',
filterLocal: true,
dataSource: [
{
defaultChecked: false,
text: {
en_US: 'Option 1',
zh_CN: '男',
type: 'i18n',
__sid__: 'param_k1owc4tb',
},
__sid__: 'serial_k1owc4t1',
value: 'M',
sid: 'opt_k1owc4t2',
},
{
defaultChecked: false,
text: {
en_US: 'Option 2',
zh_CN: '女',
type: 'i18n',
__sid__: 'param_k1owc4tf',
},
__sid__: 'serial_k1owc4t2',
value: 'F',
sid: 'opt_k1owc4t3',
},
],
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
useDetailValue: false,
searchDelay: 300,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Card',
id: 'node_k1ow3cbl',
props: {
__slot__title: false,
subTitle: {
use: 'zh_CN',
en_US: '',
zh_CN: '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
className: 'card_kh05zf9e',
title: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '部门信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1m',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbm',
props: {},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc4',
props: {
fieldName: 'department',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h21',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '所属部门',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cc5',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h22',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cc6',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjo',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc8',
props: {
fieldName: 'leader',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h23',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '主管',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cc7',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjp',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc9',
props: {
fieldName: 'hrg',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h24',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: 'HRG',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: '',
zh_CN: '',
},
maxLength: 200,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Div',
id: 'node_k1ow3cbo',
props: {
className: 'div_kh05zf9h',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
condition: true,
children: [
{
componentName: 'Button',
id: 'node_k1ow3cbn',
props: {
triggerEventsWhenLoading: false,
onClick: {
rawType: 'events',
type: 'JSExpression',
value:
'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])',
events: [
{
name: 'submit',
id: 'submit',
params: {},
type: 'actionRef',
uuid: '1570966253282_0',
},
],
},
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kh05zf9f',
type: 'primary',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh_CN',
en_US: 'Button',
zh_CN: '提交',
type: 'i18n',
},
__style__: ':root {\n margin-right: 16px;\n width: 80px\n}',
fieldId: 'button_k1ow3h1n',
},
condition: true,
},
{
componentName: 'Button',
id: 'node_k1ow3cbp',
props: {
triggerEventsWhenLoading: false,
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kh05zf9g',
type: 'normal',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh_CN',
en_US: 'Button',
zh_CN: '取消',
type: 'i18n',
},
__style__: ':root {\n width: 80px;\n}',
fieldId: 'button_k1ow3h1p',
},
condition: true,
},
],
},
],
},
],
},
{
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
},
],
};

View File

@ -0,0 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
React.PropTypes = PropTypes;
window.React = React;

View File

@ -0,0 +1 @@
export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils';

View File

@ -0,0 +1,89 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import VisualEngine, {
designer,
editor,
skeleton,
/**
* VE.Popup
*/
Popup,
/**
* VE Utils
*/
utils,
I18nUtil,
Hotkey,
Env,
monitor,
/* pub/sub 集线器 */
Bus,
/* 事件 */
EVENTS,
/* 修饰方法 */
HOOKS,
Exchange,
context,
/**
* VE.init
*
* Initialized the whole VisualEngine UI
*/
init,
ui,
Panes,
modules,
Trunk,
Prototype,
Bundle,
Pages,
DragEngine,
Viewport,
Version,
Project,
logger,
Symbols,
} from '../../src';
import { Editor } from '@ali/lowcode-editor-core';
describe('API 多种导出场景测试', () => {
it('window.VisualEngine 和 npm 导出 API 测试', () => {
expect(VisualEngine).toBe(window.VisualEngine);
});
it('npm 导出 API 对比测试', () => {
expect(VisualEngine.designer).toBe(designer);
expect(VisualEngine.editor).toBe(editor);
expect(VisualEngine.skeleton).toBe(skeleton);
expect(VisualEngine.Popup).toBe(Popup);
expect(VisualEngine.utils).toBe(utils);
expect(VisualEngine.I18nUtil).toBe(I18nUtil);
expect(VisualEngine.Hotkey).toBe(Hotkey);
expect(VisualEngine.Env).toBe(Env);
expect(VisualEngine.monitor).toBe(monitor);
expect(VisualEngine.Bus).toBe(Bus);
expect(VisualEngine.EVENTS).toBe(EVENTS);
expect(VisualEngine.HOOKS).toBe(HOOKS);
expect(VisualEngine.Exchange).toBe(Exchange);
expect(VisualEngine.context).toBe(context);
expect(VisualEngine.init).toBe(init);
expect(VisualEngine.ui).toBe(ui);
expect(VisualEngine.Panes).toBe(Panes);
expect(VisualEngine.modules).toBe(modules);
expect(VisualEngine.Trunk).toBe(Trunk);
expect(VisualEngine.Prototype).toBe(Prototype);
expect(VisualEngine.Bundle).toBe(Bundle);
expect(VisualEngine.DragEngine).toBe(DragEngine);
expect(VisualEngine.Pages).toBe(Pages);
expect(VisualEngine.Viewport).toBe(Viewport);
expect(VisualEngine.Version).toBe(Version);
expect(VisualEngine.Project).toBe(Project);
expect(VisualEngine.logger).toBe(logger);
expect(VisualEngine.Symbols).toBe(Symbols);
});
});

View File

@ -0,0 +1,161 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import formSchema from '../fixtures/schema/form';
import VisualEngine from '../../src';
import { Editor } from '@ali/lowcode-editor-core';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const pageSchema = { componentsTree: [formSchema] };
describe('VisualEngine.Pages 相关 API 测试', () => {
afterEach(() => {
VisualEngine.Pages.unload();
});
describe('addPage 系列', () => {
it('基本的节点模型初始化,初始化传入 schema', () => {
const doc = VisualEngine.Pages.addPage(pageSchema)!;
expect(doc).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(doc.nodesMap.size).toBe(expectedNodeCnt);
});
it('基本的节点模型初始化,初始化传入 schema带有 slot', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title', {
type: 'JSBlock',
value: {
componentName: 'Slot',
children: [
{
componentName: 'Text',
id: 'node_k1ow3cbf',
props: {
showTitle: false,
behavior: 'NORMAL',
content: {
type: 'variable',
value: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '个人信息',
type: 'i18n',
},
variable: 'state.title',
},
__style__: {},
fieldId: 'text_k1ow3h1j',
maxLine: 0,
},
condition: true,
},
],
props: {
slotTitle: '标题区域',
slotName: 'title',
},
},
});
const doc = VisualEngine.Pages.addPage({ componentsTree: [formSchemaWithSlot] })!;
expect(doc).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
// slot 会多出1 + N个节点
expect(doc.nodesMap.size).toBe(expectedNodeCnt + 2);
});
it('导出 schema', () => {
const doc = VisualEngine.Pages.addPage(pageSchema)!;
expect(doc).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const exportedData = doc.toData();
expect(exportedData).toHaveProperty('componentsMap');
expect(exportedData).toHaveProperty('componentsTree');
expect(exportedData.componentsTree).toHaveLength(1);
const exportedSchema = exportedData.componentsTree[0];
expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt);
});
});
describe('removePage 系列', () => {
it('removePage', () => {
const doc = VisualEngine.Pages.addPage(pageSchema)!;
expect(doc).toBeTruthy();
expect(VisualEngine.Pages.documents).toHaveLength(1);
VisualEngine.Pages.removePage(doc);
expect(VisualEngine.Pages.documents).toHaveLength(0);
});
});
describe('getPage 系列', () => {
it('getPage', () => {
const doc = VisualEngine.Pages.addPage(pageSchema);
const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
expect(VisualEngine.Pages.getPage(0)).toBe(doc);
expect(VisualEngine.Pages.getPage((_doc) => _doc.rootNode.id === 'page')).toBe(doc2);
});
});
describe('setPages 系列', () => {
it('setPages componentsTree 只有一个元素', () => {
VisualEngine.Pages.setPages([pageSchema]);
const { currentDocument } = VisualEngine.Pages;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const exportedData = currentDocument.toData();
expect(exportedData).toHaveProperty('componentsMap');
expect(exportedData).toHaveProperty('componentsTree');
expect(exportedData.componentsTree).toHaveLength(1);
const exportedSchema = exportedData.componentsTree[0];
expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt);
});
});
describe('setCurrentPage / getCurrentPage / currentPage / currentDocument 系列', () => {
it('getCurrentPage', () => {
const doc = VisualEngine.Pages.addPage(pageSchema)!;
expect(doc).toBeTruthy();
expect(doc).toBe(VisualEngine.Pages.getCurrentPage());
expect(doc).toBe(VisualEngine.Pages.currentDocument);
expect(doc).toBe(VisualEngine.Pages.currentPage);
});
it('setCurrentPage', () => {
const doc = VisualEngine.Pages.addPage(pageSchema);
expect(doc).toBe(VisualEngine.Pages.currentDocument);
const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
expect(doc2).toBe(VisualEngine.Pages.currentDocument);
VisualEngine.Pages.setCurrentPage(doc);
expect(doc).toBe(VisualEngine.Pages.currentDocument);
});
});
describe('onCurrentPageChange 系列', () => {
it('多次切换', () => {
const doc = VisualEngine.Pages.addPage(pageSchema);
const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
const docChangeHandler = jest.fn();
VisualEngine.Pages.onCurrentDocumentChange(docChangeHandler);
VisualEngine.Pages.setCurrentPage(doc);
expect(docChangeHandler).toHaveBeenCalledTimes(1);
expect(docChangeHandler).toHaveBeenLastCalledWith(doc);
VisualEngine.Pages.setCurrentPage(doc2);
expect(docChangeHandler).toHaveBeenCalledTimes(2);
expect(docChangeHandler).toHaveBeenLastCalledWith(doc2);
});
});
describe('toData 系列', () => {
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
const doc = VisualEngine.Pages.addPage(pageSchema);
const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
const dataList = VisualEngine.Pages.toData();
expect(dataList.length).toBe(2);
expect(dataList[0]).toHaveProperty('componentsMap');
expect(dataList[0]).toHaveProperty('componentsTree');
expect(dataList[0].componentsTree).toHaveLength(1);
expect(dataList[0].componentsTree[0].id).toBe('node_k1ow3cb9');
expect(dataList[1]).toHaveProperty('componentsMap');
expect(dataList[1]).toHaveProperty('componentsTree');
expect(dataList[1].componentsTree).toHaveLength(1);
expect(dataList[1].componentsTree[0].id).toBe('page');
});
});
});

View File

@ -0,0 +1,38 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import VisualEngine from '../../src';
import { Editor } from '@ali/lowcode-editor-core';
// import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
describe.skip('VisualEngine.Project 相关 API 测试', () => {
describe('getSchema / setSchema 系列', () => {
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
// console.log(VisualEngine);
// console.log(Editor instanceof Function);
// console.log(Editor.toString())
// console.log(new Editor());
// console.log(Editor2 instanceof Function);
console.log(VisualEngine.Pages.addPage(formSchema));
});
});
describe('setConfig 系列', () => {
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
// console.log(VisualEngine);
// console.log(Editor instanceof Function);
// console.log(Editor.toString())
// console.log(new Editor());
// console.log(Editor2 instanceof Function);
console.log(VisualEngine.Pages.addPage(formSchema));
});
});
});

View File

@ -54,5 +54,5 @@
"publishConfig": {
"registry": "http://registry.npm.alibaba-inc.com"
},
"homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.13.1-10/build/index.html"
"homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.13.1-11/build/index.html"
}

View File

@ -339,7 +339,7 @@ export default class BaseRender extends PureComponent {
if (refProps && typeof refProps === 'string') {
this[refProps] = ref;
}
engine && engine.props.onCompGetRef(schema, ref);
ref && engine && engine.props.onCompGetRef(schema, ref);
};
}
// scope需要传入到组件上

View File

@ -187,7 +187,7 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer {
this._componentsMap = host.designer.componentsMap;
// 需要注意的是autorun 依赖收集的是同步执行的代码,所以 await / promise / callback 里的变量不会被收集依赖
// 此例中host.designer.componentsMap 是需要被收集依赖的,否则无法响应式
await host.waitForCurrentDocument();
// await host.waitForCurrentDocument();
this.buildComponents();
}