From 66d43f2103d0aef9938f5df3e2fdb0de29ae1ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8A=9B=E7=9A=93?= Date: Fri, 16 Oct 2020 17:31:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20convertI18nObject=20fix:=20initialChildr?= =?UTF-8?q?en=20=E4=BC=A0=E5=85=A5=20settingTopEntry=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/designer/.eslintrc.js | 1 + packages/designer/build.json | 3 +- packages/designer/build.plugin.js | 7 + packages/designer/jest.config.js | 26 + packages/designer/package.json | 2 +- .../designer/src/designer/setting/utils.ts | 4 + .../src/document/node/node-children.ts | 10 +- packages/designer/src/document/node/node.ts | 18 +- .../designer/src/document/node/props/prop.ts | 11 +- .../designer/src/document/node/props/props.ts | 8 +- packages/designer/src/project/project.ts | 2 +- packages/designer/src/utils/slot.ts | 8 + packages/designer/src/utils/tree.ts | 11 +- .../tests/__mocks__/document-model.ts | 10 + packages/designer/tests/__mocks__/node.ts | 9 + .../document-model/document-model.test.ts | 17 + .../document/document-model/node.test.ts | 28 + .../designer/tests/fixtures/schema/form.js | 968 ++++++++++++++++++ packages/designer/tests/fixtures/window.ts | 18 + .../designer/tests/project/project.test.js | 86 ++ packages/designer/tests/utils/index.js | 28 + packages/designer/tsconfig.json | 10 - packages/editor-preset-vision/src/editor.ts | 4 +- packages/react-renderer/package.json | 2 +- packages/utils/src/misc.ts | 4 +- 26 files changed, 1258 insertions(+), 39 deletions(-) create mode 100644 packages/designer/build.plugin.js create mode 100644 packages/designer/jest.config.js create mode 100644 packages/designer/src/utils/slot.ts create mode 100644 packages/designer/tests/__mocks__/document-model.ts create mode 100644 packages/designer/tests/__mocks__/node.ts create mode 100644 packages/designer/tests/document/document-model/document-model.test.ts create mode 100644 packages/designer/tests/document/document-model/node.test.ts create mode 100644 packages/designer/tests/fixtures/schema/form.js create mode 100644 packages/designer/tests/fixtures/window.ts create mode 100644 packages/designer/tests/project/project.test.js create mode 100644 packages/designer/tests/utils/index.js delete mode 100644 packages/designer/tsconfig.json diff --git a/package.json b/package.json index cfd030df6..a54e7fb4d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "xima": "^0.2.15" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "tnpm": { "mode": "yarn", diff --git a/packages/designer/.eslintrc.js b/packages/designer/.eslintrc.js index c3e291107..36bb033f3 100644 --- a/packages/designer/.eslintrc.js +++ b/packages/designer/.eslintrc.js @@ -12,5 +12,6 @@ module.exports = { 'no-prototype-builtins': 1, 'no-useless-constructor': 1, 'no-empty-function': 1, + '@typescript-eslint/member-ordering': 0, } } \ No newline at end of file diff --git a/packages/designer/build.json b/packages/designer/build.json index 49a393b6b..4fbf8701e 100644 --- a/packages/designer/build.json +++ b/packages/designer/build.json @@ -2,6 +2,7 @@ "plugins": [ [ "build-plugin-component" - ] + ], + "./build.plugin.js" ] } diff --git a/packages/designer/build.plugin.js b/packages/designer/build.plugin.js new file mode 100644 index 000000000..49ec39f2d --- /dev/null +++ b/packages/designer/build.plugin.js @@ -0,0 +1,7 @@ +module.exports = ({ onGetJestConfig }) => { + // console.log('== test =='); + onGetJestConfig((jestConfig) => { + // console.log(jestConfig); + return jestConfig; + }); +}; diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js new file mode 100644 index 000000000..e48c977d3 --- /dev/null +++ b/packages/designer/jest.config.js @@ -0,0 +1,26 @@ +// jest.config.js +const { pathsToModuleNameMapper } = require('ts-jest/utils'); +// In the following statement, replace `./tsconfig` with the path to your `tsconfig` file +// which contains the path mapping (ie the `compilerOptions.paths` option): + +const esModules = ['@recore/obx-react'].join('|'); +// console.log('>>> compilerOptions', compilerOptions); +// console.log('>>> compilerOptions', pathsToModuleNameMapper(compilerOptions.paths)); +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/**', + ], +}; diff --git a/packages/designer/package.json b/packages/designer/package.json index e5af6f42c..cccb87043 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -10,7 +10,7 @@ ], "scripts": { "build": "build-scripts build --skip-demo", - "test": "jest -c jest.config.js" + "test": "build-scripts test" }, "license": "MIT", "dependencies": { diff --git a/packages/designer/src/designer/setting/utils.ts b/packages/designer/src/designer/setting/utils.ts index ec2c71bf8..4bbe8e299 100644 --- a/packages/designer/src/designer/setting/utils.ts +++ b/packages/designer/src/designer/setting/utils.ts @@ -31,6 +31,10 @@ function combineTransducer(transducer, arr, context) { } export class Transducer { + setterTransducer: any; + + context: any; + constructor(context, config) { let { setter } = config; diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts index 0cc585f70..887b49029 100644 --- a/packages/designer/src/document/node/node-children.ts +++ b/packages/designer/src/document/node/node-children.ts @@ -115,6 +115,7 @@ export class NodeChildren { } this.children.splice(i, 1); } + /** * 删除一个节点 */ @@ -122,18 +123,21 @@ export class NodeChildren { if (node.isParental()) { foreachReverse(node.children, (subNode: Node) => { subNode.remove(useMutator, purge); - }); + }, (iterable, idx) => (iterable as NodeChildren).get(idx)); + foreachReverse(node.slots, (slotNode: Node) => { + slotNode.remove(useMutator, purge); + }, (iterable, idx) => (iterable as [])[idx]); } if (purge) { // should set parent null node.internalSetParent(null, useMutator); try { - node.purge(useMutator); + node.purge(); } catch (err) { console.error(err); } } - const document = node.document; + const { document } = node; document.unlinkNode(node); document.selection.remove(node.id); document.destroyNode(node); diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index e394807a7..044237c7c 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -162,18 +162,19 @@ export class Node { this.props = new Props(this, { children: isDOMText(children) || isJSExpression(children) ? children : '', }); + this.settingEntry = this.document.designer.createSettingEntry([this]); } else { // 这里 props 被初始化两次,一次 new,一次 import,new 的实例需要给 propsReducer 的钩子去使用, // import 是为了使用钩子返回的值,并非完全幂等的操作,部分行为执行两次会有 bug, // 所以在 props 里会对 new / import 做一些区别化的解析 this.props = new Props(this, props, extras); + this.settingEntry = this.document.designer.createSettingEntry([this]); this._children = new NodeChildren(this as ParentalNode, this.initialChildren(children)); this._children.internalInitParent(); this.props.import(this.upgradeProps(this.initProps(props || {})), this.upgradeProps(extras || {})); this.setupAutoruns(); } - this.settingEntry = this.document.designer.createSettingEntry([this]); this.emitter = new EventEmitter(); } @@ -317,6 +318,7 @@ export class Node { if (this.parent) { if (this.isSlot()) { this.parent.removeSlot(this, purge); + this.parent.children.delete(this, purge, useMutator); } else { this.parent.children.delete(this, purge, useMutator); } @@ -677,13 +679,13 @@ export class Node { * 删除一个Slot节点 */ removeSlot(slotNode: Node, purge = false): boolean { - if (purge) { - // should set parent null - slotNode?.internalSetParent(null, false); - slotNode?.purge(); - } - this.document.unlinkNode(slotNode); - this.document.selection.remove(slotNode.id); + // if (purge) { + // // should set parent null + // slotNode?.internalSetParent(null, false); + // slotNode?.purge(); + // } + // this.document.unlinkNode(slotNode); + // this.document.selection.remove(slotNode.id); const i = this._slots.indexOf(slotNode); if (i < 0) { return false; diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index b46a8e6f0..b09eb787f 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -6,6 +6,7 @@ import { valueToSource } from './value-to-source'; import { Props } from './props'; import { SlotNode, Node } from '../node'; import { TransformStage } from '../transform-stage'; +import { includesSlot } from '../../../utils/slot'; export const UNSET = Symbol.for('unset'); export type UNSET = typeof UNSET; @@ -231,7 +232,7 @@ export class Prop implements IPropParent { } else if (Array.isArray(val)) { this._type = 'list'; } else if (isPlainObject(val)) { - if (isJSSlot(val) && this.options.propsMode !== 'init') { + if (isJSSlot(val) && this.options.skipSetSlot !== true) { this.setAsSlot(val); return; } @@ -293,9 +294,11 @@ export class Prop implements IPropParent { this._slotNode.import(slotSchema); } else { const { owner } = this.props; - this._slotNode = owner.document.createNode(slotSchema); - owner.addSlot(this._slotNode); - this._slotNode.internalSetSlotFor(this); + if (!includesSlot(owner, data.name)) { + this._slotNode = owner.document.createNode(slotSchema); + owner.addSlot(this._slotNode); + this._slotNode.internalSetSlotFor(this); + } } this.dispose(); } diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index fa73e0190..6a2ee2b22 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -59,9 +59,9 @@ export class Props implements IPropParent { constructor(readonly owner: Node, value?: PropsMap | PropsList | null, extras?: object) { if (Array.isArray(value)) { this.type = 'list'; - this.items = value.map(item => new Prop(this, item.value, item.name, item.spread, { propsMode: 'init' })); + this.items = value.map(item => new Prop(this, item.value, item.name, item.spread, { skipSetSlot: true })); } else if (value != null) { - this.items = Object.keys(value).map(key => new Prop(this, value[key], key, false, { propsMode: 'init' })); + this.items = Object.keys(value).map(key => new Prop(this, value[key], key, false, { skipSetSlot: true })); } if (extras) { Object.keys(extras).forEach(key => { @@ -241,8 +241,8 @@ export class Props implements IPropParent { /** * 添加值 */ - add(value: CompositeValue | null, key?: string | number, spread = false): Prop { - const prop = new Prop(this, value, key, spread); + add(value: CompositeValue | null, key?: string | number, spread = false, options: any = {}): Prop { + const prop = new Prop(this, value, key, spread, options); this.items.push(prop); return prop; } diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts index 48d57da95..278d0390b 100644 --- a/packages/designer/src/project/project.ts +++ b/packages/designer/src/project/project.ts @@ -164,7 +164,7 @@ export class Project { } createDocument(data?: RootSchema): DocumentModel { - const doc = new DocumentModel(this, data); + const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]); this.documents.push(doc); this.documentsMap.set(doc.id, doc); return doc; diff --git a/packages/designer/src/utils/slot.ts b/packages/designer/src/utils/slot.ts new file mode 100644 index 000000000..3630c0c0e --- /dev/null +++ b/packages/designer/src/utils/slot.ts @@ -0,0 +1,8 @@ +import { Node } from '../document/node/node'; + +export function includesSlot(node: Node, slotName: string | undefined): boolean { + const { slots = [] } = node; + return slots.some(slot => { + return slotName && slotName === slot?.getExtraProp('name')?.getAsString(); + }); +} diff --git a/packages/designer/src/utils/tree.ts b/packages/designer/src/utils/tree.ts index 2cd8ff515..3f41f8c4c 100644 --- a/packages/designer/src/utils/tree.ts +++ b/packages/designer/src/utils/tree.ts @@ -1,7 +1,14 @@ import { NodeChildren } from '../document/node/node-children'; -export function foreachReverse(arr: NodeChildren, fn: Function, context: any = {}) { +type IterableArray = NodeChildren | any[]; + +export function foreachReverse( + arr: IterableArray, + action: (item: any) => void, + getter: (arr: IterableArray, index: number) => any, + context: any = {}, +) { for (let i = arr.length - 1; i >= 0; i--) { - fn.call(context, arr.get(i)); + action.call(context, getter(arr, i)); } } \ No newline at end of file diff --git a/packages/designer/tests/__mocks__/document-model.ts b/packages/designer/tests/__mocks__/document-model.ts new file mode 100644 index 000000000..982bb4c1b --- /dev/null +++ b/packages/designer/tests/__mocks__/document-model.ts @@ -0,0 +1,10 @@ +export class DocumentModel { + a = 1; + c = {}; + constructor() { + console.log('xxxxxxxxxxxxxxxxxxxx'); + const b = { x: { y: 2 } }; + const c: number = 2; + this.a = b?.x?.y; + } +} \ No newline at end of file diff --git a/packages/designer/tests/__mocks__/node.ts b/packages/designer/tests/__mocks__/node.ts new file mode 100644 index 000000000..4f225766b --- /dev/null +++ b/packages/designer/tests/__mocks__/node.ts @@ -0,0 +1,9 @@ +export class Node2 { + a = 1; + c = {}; + constructor() { + const b = { x: { y: 2 } }; + const c: number = 2; + this.a = b?.x?.y; + } +} \ No newline at end of file diff --git a/packages/designer/tests/document/document-model/document-model.test.ts b/packages/designer/tests/document/document-model/document-model.test.ts new file mode 100644 index 000000000..ca0c9f173 --- /dev/null +++ b/packages/designer/tests/document/document-model/document-model.test.ts @@ -0,0 +1,17 @@ +import '../../fixtures/window'; +console.log('window.matchMedia', window.matchMedia); +window.matchMedia('width=600px'); +import { DocumentModel } from '../../../src/document/document-model'; +// const { DocumentModel } = require('../../../src/document/document-model'); +// const { Node } = require('../__mocks__/node'); + +describe('basic utility', () => { + test.only('delegateMethod - useOriginMethodName', () => { + + const node = new DocumentModel({}, { + componentName: 'Component', + }); + console.log(node); + expect(1).toBe(1); + }); +}); diff --git a/packages/designer/tests/document/document-model/node.test.ts b/packages/designer/tests/document/document-model/node.test.ts new file mode 100644 index 000000000..e183aa6bd --- /dev/null +++ b/packages/designer/tests/document/document-model/node.test.ts @@ -0,0 +1,28 @@ +import '../../fixtures/window'; +import { DocumentModel } from '../../../src/document/document-model'; +import { Node } from '../../../src/document/node/node'; +// import { Node2 } from './__mocks__/node'; + +jest.mock('../../../src/document/document-model', () => { + return { + DocumentModel: jest.fn().mockImplementation(() => { + return { + project: { + designer: { createSettingEntry() {}, transformProps() {} }, + getSchema() {}, + }, + nextId() {}, + }; + }), + }; +}); + +describe('basic utility', () => { + test('delegateMethod - useOriginMethodName', () => { + const dm = new DocumentModel({} as any, {} as any); + console.log(dm.nextId); + const node = new Node(dm, { componentName: 'Leaf' }); + console.log(node); + expect(1).toBe(1); + }); +}); diff --git a/packages/designer/tests/fixtures/schema/form.js b/packages/designer/tests/fixtures/schema/form.js new file mode 100644 index 000000000..8438c8b39 --- /dev/null +++ b/packages/designer/tests/fixtures/schema/form.js @@ -0,0 +1,968 @@ +export default { + componentName: 'Page', + id: 'node_k1ow3cb9', + props: { + extensions: { + 启用页头: true, + }, + pageStyle: { + backgroundColor: '#f2f3f5', + }, + containerStyle: {}, + className: 'page_kgaqfbm4', + 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_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\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: { + // type: 'JSSlot', + value: [ + { + componentName: 'Text', + id: 'node_k1ow3cbf', + props: { + showTitle: false, + behavior: 'NORMAL', + content: { + use: 'zh_CN', + en_US: 'Title', + zh_CN: '个人信息', + type: 'i18n', + }, + __style__: {}, + fieldId: 'text_k1ow3h1j', + maxLine: 0, + }, + condition: true, + }, + ], + }, + content: '', + __slot__logo: false, + __slot__crumb: false, + crumb: '', + tab: '', + logo: '', + action: '', + __slot__tab: false, + __style__: {}, + __slot__content: false, + fieldId: 'pageHeader_k1ow3h1i', + subTitle: false, + }, + 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_kgaqfbm5', + 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: '6:6', + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + 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_kgaqfbm6', + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + }, + 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: null, + zh_CN: '', + }, + }, + condition: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'Div', + id: 'node_k1ow3cbo', + props: { + className: 'div_kgaqfbm9', + 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_kgaqfbm7', + 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_kgaqfbm8', + 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, + }, + ], +}; diff --git a/packages/designer/tests/fixtures/window.ts b/packages/designer/tests/fixtures/window.ts new file mode 100644 index 000000000..d772d1ef4 --- /dev/null +++ b/packages/designer/tests/fixtures/window.ts @@ -0,0 +1,18 @@ +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +Object.defineProperty(window, 'React', { + writable: true, + value: {}, +}); \ No newline at end of file diff --git a/packages/designer/tests/project/project.test.js b/packages/designer/tests/project/project.test.js new file mode 100644 index 000000000..eafa8aa3a --- /dev/null +++ b/packages/designer/tests/project/project.test.js @@ -0,0 +1,86 @@ +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 { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('schema 渲染测试', () => { + it('最简单的例子,练手用', () => { + const project = new Project(designer, { + componentsTree: [{ + componentName: 'Page', + id: 'page_id', + props: { + name: 'haha', + }, + children: [{ + componentName: 'Div', + id: 'div_id', + props: { + name: 'div from haha', + }, + }], + }], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + // console.log(project.currentDocument.nodesMap.get('div_id').props.items); + expect(nodesMap.has('page_id')).toBeTruthy; + expect(nodesMap.has('div_id')).toBeTruthy; + expect(mockCreateSettingEntry).toBeCalledTimes(2); + // console.log(currentDocument.export(3)); + }); + + it.only('普通场景,无 block / component,无 slot', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + ids.forEach(id => { + expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); + }); + // console.log(nodesMap.get('node_k1ow3cb9').componentName, getNodeFromSchemaById(formSchema, 'node_k1ow3cb9').componentName) + console.log(nodesMap.size); + // expect(nodesMap.has('page_id')).toBeTruthy; + // expect(nodesMap.has('div_id')).toBeTruthy; + // expect(mockCreateSettingEntry).toBeCalledTimes(2); + // console.log(currentDocument.export(3)); + }); + + it('普通场景,无 block / component,有 slot', () => {}); + it('普通场景,无 block / component,有 slot', () => {}); +}); \ No newline at end of file diff --git a/packages/designer/tests/utils/index.js b/packages/designer/tests/utils/index.js new file mode 100644 index 000000000..f8fb61e4e --- /dev/null +++ b/packages/designer/tests/utils/index.js @@ -0,0 +1,28 @@ +export function getIdsFromSchema(schema, ids = []) { + if (!schema) return ids; + const { componentName, id, children } = schema; + if (componentName) { + ids.push(id); + } + if (Array.isArray(children) && children.length > 0) { + children.forEach(node => getIdsFromSchema(node, ids)); + } + return ids; +} + +export function getNodeFromSchemaById(schema, _id) { + if (!schema) return null; + const { id, children } = schema; + let retNode = null; + if (_id === id) return schema; + if (Array.isArray(children) && children.length > 0) { + children.some(node => { + retNode = getNodeFromSchemaById(node, _id); + if (retNode) { + return true; + } + return false; + }); + } + return retNode; +} \ No newline at end of file diff --git a/packages/designer/tsconfig.json b/packages/designer/tsconfig.json deleted file mode 100644 index 4a965ec62..000000000 --- a/packages/designer/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "lib" - }, - "include": [ - "./src/" - ] -} - diff --git a/packages/editor-preset-vision/src/editor.ts b/packages/editor-preset-vision/src/editor.ts index 24cb6169d..7a66128cd 100644 --- a/packages/editor-preset-vision/src/editor.ts +++ b/packages/editor-preset-vision/src/editor.ts @@ -125,7 +125,7 @@ designer.addPropsReducer((props, node) => { !isI18NObject(ov) && !isJSExpression(ov) && !isVariable(ov)) { - newProps[item.name] = v; + newProps[item.name] = convertToI18NObject(v); } } catch (e) { if (hasOwnProperty(props, item.name)) { @@ -133,7 +133,7 @@ designer.addPropsReducer((props, node) => { } } if (newProps[item.name] && !node.props.has(item.name)) { - node.props.add(newProps[item.name], item.name); + node.props.add(newProps[item.name], item.name, false, { skipSetSlot: true }); } }); } diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index d1920c442..56cbdccf3 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -54,5 +54,5 @@ "publishConfig": { "registry": "http://registry.npm.alibaba-inc.com" }, - "homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.12.1-3/build/index.html" + "homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.13.1-1/build/index.html" } diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 574def94a..e059a062d 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -1,4 +1,5 @@ +import { isI18NObject } from './is-object'; export function isUseI18NSetter(prototype: any, propName: string) { const configure = prototype?.options?.configure; @@ -10,6 +11,7 @@ export function isUseI18NSetter(prototype: any, propName: string) { return false; } -export function convertToI18NObject(v: string, locale: string = 'zh_CN') { +export function convertToI18NObject(v: string | object, locale: string = 'zh_CN') { + if (isI18NObject(v)) return v; return { type: 'i18n', use: locale, [locale]: v }; }