diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts index 4246a2423..805042440 100644 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ b/packages/designer/src/designer/setting/setting-prop-entry.ts @@ -86,7 +86,7 @@ export class SettingPropEntry implements SettingEntry { } const propName = this.path.join('.'); let l = this.nodes.length; - while (l-- > 1) { + while (l-- > 0) { this.nodes[l].getProp(propName, true)!.key = key; } this._name = key; diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index 6a2ee2b22..ed44aa874 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -15,10 +15,14 @@ export function getConvertedExtraKey(key: string): string { if (key.indexOf('.') > 0) { _key = key.split('.')[0]; } - return EXTRA_KEY_PREFIX + _key + EXTRA_KEY_PREFIX + key.substr(_key.length); + return EXTRA_KEY_PREFIX + _key + EXTRA_KEY_PREFIX + key.substr(_key.length + 1); } export function getOriginalExtraKey(key: string): string { - return key.replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), ''); + // 移除串首、串尾的 EXTRA_KEY_PREFIX,将剩下的转成 . + return key + .replace(new RegExp(`^${EXTRA_KEY_PREFIX}`), '') + .replace(new RegExp(`${EXTRA_KEY_PREFIX}$`), '') + .replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), '.'); } export class Props implements IPropParent { diff --git a/packages/designer/tests/designer/dragon.test.ts b/packages/designer/tests/designer/dragon.test.ts new file mode 100644 index 000000000..f9774be60 --- /dev/null +++ b/packages/designer/tests/designer/dragon.test.ts @@ -0,0 +1,14 @@ +import { fireEvent } from '@testing-library/react'; + +it('test', () => { + document.addEventListener('keydown', (e) => { + console.log(e); + }) + + fireEvent.keyDown(document, { key: 'Enter', }) + + document.addEventListener('drag', (e) => { + console.log(e); + }) + fireEvent.drag(document, {}) +}) \ No newline at end of file diff --git a/packages/designer/tests/designer/setting-entry/setting-prop-entry.test.ts b/packages/designer/tests/designer/setting/setting-prop-entry.test.ts similarity index 100% rename from packages/designer/tests/designer/setting-entry/setting-prop-entry.test.ts rename to packages/designer/tests/designer/setting/setting-prop-entry.test.ts diff --git a/packages/designer/tests/designer/setting-entry/setting-top-entry.test.ts b/packages/designer/tests/designer/setting/setting-top-entry.test.ts similarity index 100% rename from packages/designer/tests/designer/setting-entry/setting-top-entry.test.ts rename to packages/designer/tests/designer/setting/setting-top-entry.test.ts diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts new file mode 100644 index 000000000..d2a8c2f30 --- /dev/null +++ b/packages/designer/tests/document/node/props/prop.test.ts @@ -0,0 +1,394 @@ +import '../../../fixtures/window'; +import { set } from '../../../utils'; +import { Editor } from '@ali/lowcode-editor-core'; +import { Props } from '../../../../src/document/node/props/props'; +import { Designer } from '../../../../src/designer/designer'; +import { Project } from '../../../../src/project/project'; +import { DocumentModel } from '../../../../src/document/document-model'; +import { Prop, isProp, isValidArrayIndex } from '../../../../src/document/node/props/prop'; +import { TransformStage } from '@ali/lowcode-types'; +import { delayObxTick } from '../../../utils'; + +const mockedOwner = { + componentName: 'Div', +}; + +const mockedPropsInst = { + owner: mockedOwner, +}; +mockedPropsInst.props = mockedPropsInst; + +describe('Prop 类测试', () => { + describe('基础类型', () => { + let boolProp: Prop; + let strProp: Prop; + let numProp: Prop; + let nullProp: Prop; + let expProp: Prop; + let slotProp: Prop; + beforeEach(() => { + boolProp = new Prop(mockedPropsInst, true, 'boolProp'); + strProp = new Prop(mockedPropsInst, 'haha', 'strProp'); + numProp = new Prop(mockedPropsInst, 1, 'numProp'); + nullProp = new Prop(mockedPropsInst, null, 'nullProp'); + expProp = new Prop(mockedPropsInst, { type: 'JSExpression', value: 'state.haha' }, 'expProp'); + // slotProp = new Prop(mockedPropsInst, { type: 'JSSlot', value: [{ componentName: 'Button' }] }, 'slotProp'); + }); + afterEach(() => { + boolProp.purge(); + strProp.purge(); + numProp.purge(); + nullProp.purge(); + expProp.purge(); + // slotProp.purge(); + }); + + it('consturctor / getProps / getNode', () => { + expect(boolProp.parent).toBe(mockedPropsInst); + expect(boolProp.getProps()).toBe(mockedPropsInst); + expect(boolProp.getNode()).toBe(mockedOwner); + }); + + it('misc', () => { + expect(boolProp.get('x', false)).toBeNull(); + expect(boolProp.maps).toBeNull(); + expect(boolProp.add()).toBeNull(); + + strProp.unset(); + strProp.add(2, true); + strProp.set(1); + + expect(numProp.set()).toBeNull(); + expect(numProp.has()).toBeFalsy(); + }); + + it('getValue / getAsString / setValue', () => { + expect(strProp.getValue()).toBe('haha'); + strProp.setValue('heihei'); + expect(strProp.getValue()).toBe('heihei'); + expect(strProp.getAsString()).toBe('heihei'); + + strProp.unset(); + expect(strProp.getValue()).toBeUndefined(); + }); + + it('code', () => { + expect(expProp.code).toBe('state.haha'); + expect(boolProp.code).toBe('true'); + expect(strProp.code).toBe('"haha"'); + + expProp.code = 'state.heihei'; + expect(expProp.code).toBe('state.heihei'); + expect(expProp.getValue()).toEqual({ + type: 'JSExpression', + value: 'state.heihei', + }); + + boolProp.code = 'false'; + expect(boolProp.code).toBe('false'); + expect(boolProp.getValue()).toBe(false); + + strProp.code = '"heihei"'; + expect(strProp.code).toBe('"heihei"'); + expect(strProp.getValue()).toBe('heihei'); + + // TODO: 不确定为什么会有这个分支 + strProp.code = 'state.a'; + expect(strProp.code).toBe('state.a'); + expect(strProp.getValue()).toEqual({ + type: 'JSExpression', + value: 'state.a', + mock: 'heihei', + }); + }); + + it('export', () => { + expect(boolProp.export(TransformStage.Save)).toBe(true); + expect(strProp.export(TransformStage.Save)).toBe('haha'); + expect(numProp.export(TransformStage.Save)).toBe(1); + expect(nullProp.export(TransformStage.Save)).toBe(''); + expect(nullProp.export(TransformStage.Serilize)).toBe(null); + expect(expProp.export(TransformStage.Save)).toEqual({ + type: 'JSExpression', + value: 'state.haha', + }); + + strProp.unset(); + expect(strProp.getValue()).toBeUndefined(); + expect(strProp.isUnset()).toBeTruthy(); + expect(strProp.export(TransformStage.Save)).toBeUndefined(); + + expect( + new Prop(mockedPropsInst, false, '___condition___').export(TransformStage.Render), + ).toBeTruthy(); + // console.log(slotProp.export(TransformStage.Render)); + }); + + it('compare', () => { + const newProp = new Prop(mockedPropsInst, 'haha'); + expect(strProp.compare(newProp)).toBe(0); + expect(strProp.compare(expProp)).toBe(2); + + newProp.unset(); + expect(strProp.compare(newProp)).toBe(2); + strProp.unset(); + expect(strProp.compare(newProp)).toBe(0); + }); + + it('isVirtual', () => { + expect(new Prop(mockedPropsInst, 111, '!virtualProp')).toBeTruthy(); + }); + + it('purge', () => { + boolProp.purge(); + expect(boolProp.purged).toBeTruthy(); + boolProp.purge(); + }); + + it('迭代器 / map / forEach', () => { + const mockedFn = jest.fn(); + for (let item of strProp) { + mockedFn(); + } + expect(mockedFn).not.toHaveBeenCalled(); + mockedFn.mockClear(); + + strProp.forEach(item => { + mockedFn(); + }); + expect(mockedFn).not.toHaveBeenCalled(); + mockedFn.mockClear(); + + strProp.map(item => { + mockedFn(); + }); + expect(mockedFn).not.toHaveBeenCalled(); + mockedFn.mockClear(); + }); + }); + + describe('复杂类型', () => { + describe('items(map 类型)', () => { + let prop: Prop; + beforeEach(() => { + prop = new Prop(mockedPropsInst, { + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }); + }); + afterEach(() => { + prop.purge(); + }); + + it('items / get', async () => { + expect(prop.size).toBe(5); + + expect(prop.get('a').getValue()).toBe(1); + expect(prop.get('b').getValue()).toBe('str'); + expect(prop.get('c').getValue()).toBe(true); + expect(prop.get('d').getValue()).toEqual({ type: 'JSExpression', value: 'state.a' }); + expect(prop.get('z').getValue()).toEqual({ + z1: 1, + z2: 'str', + }); + + + expect(prop.getPropValue('a')).toBe(1); + prop.setPropValue('a', 2); + expect(prop.getPropValue('a')).toBe(2); + prop.clearPropValue('a'); + expect(prop.get('a')?.isUnset()).toBeTruthy(); + + expect(prop.get('z.z1')?.getValue()).toBe(1); + expect(prop.get('z.z2')?.getValue()).toBe('str'); + + const fromStashProp = prop.get('l'); + const fromStashNestedProp = prop.get('m.m1'); + fromStashProp.setValue('fromStashProp'); + fromStashNestedProp?.setValue('fromStashNestedProp') + + await delayObxTick(); + expect(prop.get('l').getValue()).toBe('fromStashProp'); + expect(prop.get('m.m1').getValue()).toBe('fromStashNestedProp'); + }); + + it('export', () => { + // TODO: 需要访问一下才能触发构造 _items + prop.items; + expect(prop.export()).toEqual({ + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }); + }); + + it('compare', () => { + const prop1 = new Prop(mockedPropsInst, { a: 1 }); + const prop2 = new Prop(mockedPropsInst, { b: 1 }); + expect(prop1.compare(prop2)).toBe(1); + }); + + it('has / add / delete / deleteKey / remove', () => { + expect(prop.has('a')).toBeTruthy(); + expect(prop.has('b')).toBeTruthy(); + expect(prop.has('c')).toBeTruthy(); + expect(prop.has('d')).toBeTruthy(); + expect(prop.has('z')).toBeTruthy(); + expect(prop.has('y')).toBeFalsy(); + + // 触发一下内部 maps 构造 + prop.items; + expect(prop.has('z')).toBeTruthy(); + + expect(prop.add(1)).toBeNull(); + + prop.deleteKey('c'); + expect(prop.get('c', false)).toBeNull(); + prop.delete(prop.get('b')); + expect(prop.get('b', false)).toBeNull(); + + prop.get('d')?.remove(); + expect(prop.get('d', false)).toBeNull(); + }); + + it('set', () => { + prop.set('e', 1); + expect(prop.get('e', false)?.getValue()).toBe(1); + prop.set('a', 5); + expect(prop.get('a', false)?.getValue()).toBe(5); + }); + + it('迭代器 / map / forEach', () => { + const mockedFn = jest.fn(); + for (let item of prop) { + mockedFn(); + } + expect(mockedFn).toHaveBeenCalledTimes(5); + mockedFn.mockClear(); + + prop.forEach(item => { + mockedFn(); + }); + expect(mockedFn).toHaveBeenCalledTimes(5); + mockedFn.mockClear(); + + prop.map(item => { + mockedFn(); + }); + expect(mockedFn).toHaveBeenCalledTimes(5); + mockedFn.mockClear(); + }); + + it('dispose', () => { + prop.dispose(); + + expect(prop._items).toBeNull(); + expect(prop._maps).toBeNull(); + }); + }); + + describe('items(list 类型)', () => { + let prop: Prop; + beforeEach(() => { + prop = new Prop(mockedPropsInst, [1, true, 'haha']); + }); + afterEach(() => { + prop.purge(); + }); + + it('items / get', () => { + expect(prop.size).toBe(3); + + expect(prop.get(0).getValue()).toBe(1); + expect(prop.get(1).getValue()).toBe(true); + expect(prop.get(2).getValue()).toBe('haha'); + + expect(prop.getAsString()).toBe(''); + }); + + it('export', () => { + // 触发构造 + prop.items; + expect(prop.export()).toEqual([1, true, 'haha']); + }); + + it('compare', () => { + const prop1 = new Prop(mockedPropsInst, [1]); + const prop2 = new Prop(mockedPropsInst, [2]); + const prop3 = new Prop(mockedPropsInst, [1, 2]); + expect(prop1.compare(prop2)).toBe(1); + expect(prop1.compare(prop3)).toBe(2); + }); + + it('set', () => { + prop.set(0, 1); + expect(prop.get(0, false)?.getValue()).toBe(1); + // illegal + // expect(prop.set(5, 1)).toBeNull(); + }); + }); + }); + + describe('slotNode / setAsSlot', () => { + const editor = new Editor(); + const designer = new Designer({ editor }); + const doc = new DocumentModel(designer.project, { + componentName: 'Page', + children: [{ + id: 'div', + componentName: 'Div', + }], + }); + const div = doc.getNode('div'); + + const slotProp = new Prop(div?.getProps(), { + type: 'JSSlot', + value: [{ + componentName: 'Button' + }], + }); + + expect(slotProp.slotNode?.componentName).toBe('Slot'); + + // TODO: id 总是变,不好断言 + expect(slotProp.code.includes('Button')).toBeTruthy(); + + console.log(slotProp.export()); + + expect(slotProp.export().value[0].componentName).toBe('Button'); + expect(slotProp.export(TransformStage.Serilize).value[0].componentName).toBe('Button'); + + slotProp.purge(); + expect(slotProp.purged).toBeTruthy(); + slotProp.dispose(); + }); +}); + +describe('其他导出函数', () => { + it('isProp', () => { + expect(isProp({ isProp: true })).toBeTruthy(); + }); + + it('isValidArrayIndex', () => { + expect(isValidArrayIndex('1')).toBeTruthy(); + expect(isValidArrayIndex('1', 2)).toBeTruthy(); + expect(isValidArrayIndex('2', 1)).toBeFalsy(); + }); +}); diff --git a/packages/designer/tests/document/node/props/props.test.ts b/packages/designer/tests/document/node/props/props.test.ts new file mode 100644 index 000000000..c72104072 --- /dev/null +++ b/packages/designer/tests/document/node/props/props.test.ts @@ -0,0 +1,244 @@ + +import '../../../fixtures/window'; +import { set } from '../../../utils'; +import { Editor } from '@ali/lowcode-editor-core'; +import { Props, getConvertedExtraKey, getOriginalExtraKey } from '../../../../src/document/node/props/props'; +import { Designer } from '../../../../src/designer/designer'; +import { Project } from '../../../../src/project/project'; +import { DocumentModel } from '../../../../src/document/document-model'; +import { Prop, isProp, isValidArrayIndex } from '../../../../src/document/node/props/props'; +import { TransformStage } from '@ali/lowcode-types'; +import { delayObxTick } from '../../../utils'; + +const mockedOwner = { componentName: 'Page' }; + +describe('Props 类测试', () => { + let props: Props; + beforeEach(() => { + props = new Props(mockedOwner, { + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }, { condition: true }); + }); + afterEach(() => { + props.purge(); + }); + + it('getNode', () => { + expect(props.getNode()).toBe(mockedOwner); + }); + + it('items / get', async () => { + expect(props.size).toBe(6); + + expect(props.get('a').getValue()).toBe(1); + expect(props.get('b').getValue()).toBe('str'); + expect(props.get('c').getValue()).toBe(true); + expect(props.get('d').getValue()).toEqual({ type: 'JSExpression', value: 'state.a' }); + expect(props.get('z').getValue()).toEqual({ + z1: 1, + z2: 'str', + }); + + + expect(props.getPropValue('a')).toBe(1); + props.setPropValue('a', 2); + expect(props.getPropValue('a')).toBe(2); + // props.clearPropValue('a'); + // expect(props.get('a')?.isUnset()).toBeTruthy(); + + expect(props.get('z.z1')?.getValue()).toBe(1); + expect(props.get('z.z2')?.getValue()).toBe('str'); + + const fromStashProp = props.get('l', true); + const fromStashNestedProp = props.get('m.m1', true); + fromStashProp.setValue('fromStashProp'); + fromStashNestedProp?.setValue('fromStashNestedProp') + + await delayObxTick(); + expect(props.get('l').getValue()).toBe('fromStashProp'); + expect(props.get('m.m1').getValue()).toBe('fromStashNestedProp'); + }); + + it('export', () => { + expect(props.export()).toEqual({ + props: { + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }, + extras: { + condition: true, + }, + }); + + expect(props.toData()).toEqual({ + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }); + + props.get('a')?.unset(); + expect(props.toData()).toEqual({ + a: undefined, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, + }); + }); + + it('import', () => { + props.import({ + x: 1, + y: true + }, { loop: false }); + expect(props.export()).toEqual({ + props: { + x: 1, + y: true, + }, + extras: { + loop: false, + } + }); + + props.import(); + }); + + it('merge', async () => { + props.merge({ x: 1 }); + + await delayObxTick(); + + expect(props.get('x')?.getValue()).toBe(1); + }); + + it('has / add / delete / deleteKey / remove', () => { + expect(props.has('a')).toBeTruthy(); + expect(props.has('b')).toBeTruthy(); + expect(props.has('c')).toBeTruthy(); + expect(props.has('d')).toBeTruthy(); + expect(props.has('z')).toBeTruthy(); + expect(props.has('y')).toBeFalsy(); + + props.add(1, 'newAdded'); + expect(props.has('newAdded')).toBeTruthy(); + + props.deleteKey('c'); + expect(props.get('c', false)).toBeNull(); + props.delete(props.get('b')); + expect(props.get('b', false)).toBeNull(); + + props.get('d')?.remove(); + expect(props.get('d', false)).toBeNull(); + }); + + it('迭代器 / map / forEach', () => { + const mockedFn = jest.fn(); + for (let item of props) { + mockedFn(); + } + expect(mockedFn).toHaveBeenCalledTimes(6); + mockedFn.mockClear(); + + props.forEach(item => { + mockedFn(); + }); + expect(mockedFn).toHaveBeenCalledTimes(6); + mockedFn.mockClear(); + + props.map(item => { + mockedFn(); + }); + expect(mockedFn).toHaveBeenCalledTimes(6); + mockedFn.mockClear(); + + props.filter(item => { + mockedFn(); + }); + expect(mockedFn).toHaveBeenCalledTimes(6); + mockedFn.mockClear(); + }); + + it('purge', () => { + props.purge(); + + expect(props.purged).toBeTruthy(); + }); + + it('empty items', () => { + expect(new Props(mockedOwner).export()).toEqual({}); + }); + + describe('list 类型', () => { + let props: Props; + beforeEach(() => { + props = new Props(mockedOwner, [1, true, 'haha'], { condition: true }); + }); + it('constructor', () => { + props.purge(); + }); + + it('export', () => { + expect(props.export().extras).toEqual({ + condition: true + }) + }); + + it('import', () => { + props.import([1], { loop: true }); + expect(props.export().extras).toEqual({ + loop: true + }); + + props.items[0]?.unset(); + props.export(); + }); + }); +}); + + +describe('其他函数', () => { + it('getConvertedExtraKey', () => { + expect(getConvertedExtraKey()).toBe(''); + expect(getConvertedExtraKey('a')).toBe('___a___'); + expect(getConvertedExtraKey('a.b')).toBe('___a___b'); + }); + + it('getOriginalExtraKey', () => { + expect(getOriginalExtraKey('___a___')).toBe('a'); + expect(getOriginalExtraKey('___a___b')).toBe('a.b'); + }); +}); \ No newline at end of file diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index aa758fe16..263a71e56 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -1,6 +1,8 @@ +import { Node } from '@ali/lowcode-designer'; import LowCodeRenderer from '@ali/lowcode-react-renderer'; import { ReactInstance, Fragment, Component, createElement } from 'react'; import { observer } from '@recore/obx-react'; +import { isFromVC } from '@ali/lowcode-utils'; import { SimulatorRendererContainer, DocumentInstance } from './renderer'; import { Router, Route, Switch } from 'react-router'; import './renderer.less'; @@ -103,7 +105,7 @@ class Layout extends Component<{ rendererContainer: SimulatorRendererContainer } if (layout) { const { Component, props, componentName } = layout; if (Component) { - return {children}; + return {children}; } if (componentName && rendererContainer.getComponent(componentName)) { return createElement( @@ -147,8 +149,10 @@ class Renderer extends Component<{ customCreateElement={(Component: any, props: any, children: any) => { const { __id, __desingMode, ...viewProps } = props; viewProps.componentId = __id; - const leaf = documentInstance.getNode(__id); - viewProps._leaf = leaf; + const leaf = documentInstance.getNode(__id) as Node; + if (isFromVC(leaf?.componentMeta)) { + viewProps._leaf = leaf; + } viewProps._componentName = leaf?.componentName; // 如果是容器 && 无children && 高宽为空 增加一个占位容器,方便拖动 if ( diff --git a/packages/react-simulator-renderer/src/utils/misc.ts b/packages/react-simulator-renderer/src/utils/misc.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index a0f8f13ff..5428be93a 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -1,6 +1,7 @@ import { isI18NObject } from './is-object'; import get from 'lodash.get'; +import { ComponentMeta } from '@ali/lowcode-designer'; export function isUseI18NSetter(prototype: any, propName: string) { const configure = prototype?.options?.configure; @@ -43,3 +44,11 @@ export function waitForThing(obj: any, path: string): Promise { } return _innerWaitForThing(obj, path); } + +/** + * 判断当前 meta 是否从 vc prototype 转换而来 + * @param meta + */ +export function isFromVC(meta: ComponentMeta) { + return !!meta?.getMetadata()?.experimental; +} \ No newline at end of file