From b29c53901e4a1c1f437d9fe71aadaa16d02686f4 Mon Sep 17 00:00:00 2001 From: ZeralZhang Date: Fri, 26 Jan 2024 09:38:37 +0800 Subject: [PATCH 1/8] fix(react-simulator-renderer): detached node has children detached node has children will return false, causing memory leaks. --- packages/react-simulator-renderer/src/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-simulator-renderer/src/renderer.ts b/packages/react-simulator-renderer/src/renderer.ts index efebeda04..20f6e18c0 100644 --- a/packages/react-simulator-renderer/src/renderer.ts +++ b/packages/react-simulator-renderer/src/renderer.ts @@ -614,7 +614,7 @@ function getNodeInstance(fiberNode: any, specId?: string): IPublicTypeNodeInstan function checkInstanceMounted(instance: any): boolean { if (isElement(instance)) { - return instance.parentElement != null; + return instance.parentElement != null && window.document.contains(instance); } return true; } From b97570f10c1035991d17aba49031edd0ef710024 Mon Sep 17 00:00:00 2001 From: liujuping Date: Fri, 26 Jan 2024 15:20:13 +0800 Subject: [PATCH 2/8] docs(demo): add dialog use desc --- docs/docs/demoUsage/makeStuff/dialog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/demoUsage/makeStuff/dialog.md b/docs/docs/demoUsage/makeStuff/dialog.md index 56303067c..da78cc8e8 100644 --- a/docs/docs/demoUsage/makeStuff/dialog.md +++ b/docs/docs/demoUsage/makeStuff/dialog.md @@ -2,6 +2,8 @@ title: 3. 如何通过按钮展示/隐藏弹窗 sidebar_position: 1 --- +> 说明:这个方式依赖低代码弹窗组件是否对外保留了相关的 API,不同的物料支持的方式不一样,这里只针对综合场景的弹窗物料。 + ## 1.拖拽一个按钮 ![image.png](https://img.alicdn.com/imgextra/i1/O1CN01kLaWA31D6WwTui9VW_!!6000000000167-2-tps-3584-1812.png) From 739572172a9901c7223f7bc1617495b8e8752900 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Jan 2024 08:40:59 +0000 Subject: [PATCH 3/8] chore(docs): publish documentation --- docs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/package.json b/docs/package.json index 69dddfce2..ce6a969c1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine-docs", - "version": "1.2.29", + "version": "1.2.30", "description": "低代码引擎版本化文档", "license": "MIT", "files": [ From 6e89d4d605760d376fedf350b99f24bc11770ec5 Mon Sep 17 00:00:00 2001 From: liujuping Date: Fri, 26 Jan 2024 15:14:16 +0800 Subject: [PATCH 4/8] feat(setter): add field ts --- packages/editor-core/src/di/setter.ts | 6 +++--- .../src/components/settings/settings-pane.tsx | 4 ++-- packages/editor-skeleton/src/transducers/parse-props.ts | 3 ++- packages/types/src/shell/type/field-extra-props.ts | 2 +- packages/types/src/shell/type/registered-setter.ts | 7 +++++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/editor-core/src/di/setter.ts b/packages/editor-core/src/di/setter.ts index 437d9a89e..5af2c0230 100644 --- a/packages/editor-core/src/di/setter.ts +++ b/packages/editor-core/src/di/setter.ts @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { IPublicApiSetters, IPublicTypeCustomView, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; +import { IPublicApiSetters, IPublicModelSettingField, IPublicTypeCustomView, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; import { createContent, isCustomView } from '@alilc/lowcode-utils'; const settersMap = new Map { + setter.initialValue = (field: IPublicModelSettingField) => { return initial.call(field, field.getValue()); }; } @@ -81,7 +81,7 @@ export class Setters implements ISetters { if (!setter.initialValue) { const initial = getInitialFromSetter(setter.component); if (initial) { - setter.initialValue = (field: any) => { + setter.initialValue = (field: IPublicModelSettingField) => { return initial.call(field, field.getValue()); }; } diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-pane.tsx index 1d651bb5a..1561bf8bb 100644 --- a/packages/editor-skeleton/src/components/settings/settings-pane.tsx +++ b/packages/editor-skeleton/src/components/settings/settings-pane.tsx @@ -225,7 +225,7 @@ class SettingFieldView extends Component { if (initialValue == null) { diff --git a/packages/editor-skeleton/src/transducers/parse-props.ts b/packages/editor-skeleton/src/transducers/parse-props.ts index d22f07f43..573d24ac6 100644 --- a/packages/editor-skeleton/src/transducers/parse-props.ts +++ b/packages/editor-skeleton/src/transducers/parse-props.ts @@ -9,6 +9,7 @@ import { IPublicTypeTransformedComponentMetadata, IPublicTypeOneOfType, ConfigureSupportEvent, + IPublicModelSettingField, } from '@alilc/lowcode-types'; function propConfigToFieldConfig(propConfig: IPublicTypePropConfig): IPublicTypeFieldConfig { @@ -102,7 +103,7 @@ function propTypeToSetter(propType: IPublicTypePropType): IPublicTypeSetterType }, }, isRequired, - initialValue: (field: any) => { + initialValue: (field: IPublicModelSettingField) => { const data: any = {}; items.forEach((item: any) => { let initial = item.defaultValue; diff --git a/packages/types/src/shell/type/field-extra-props.ts b/packages/types/src/shell/type/field-extra-props.ts index 3e2df280b..7aae7e0fe 100644 --- a/packages/types/src/shell/type/field-extra-props.ts +++ b/packages/types/src/shell/type/field-extra-props.ts @@ -77,5 +77,5 @@ export interface IPublicTypeFieldExtraProps { /** * onChange 事件 */ - onChange?: (value: any, field: any) => void; + onChange?: (value: any, field: IPublicModelSettingField) => void; } diff --git a/packages/types/src/shell/type/registered-setter.ts b/packages/types/src/shell/type/registered-setter.ts index 85cad5a80..55a90465a 100644 --- a/packages/types/src/shell/type/registered-setter.ts +++ b/packages/types/src/shell/type/registered-setter.ts @@ -1,17 +1,20 @@ +import { IPublicModelSettingField } from '../model'; import { IPublicTypeCustomView, IPublicTypeTitleContent } from './'; export interface IPublicTypeRegisteredSetter { component: IPublicTypeCustomView; defaultProps?: object; title?: IPublicTypeTitleContent; + /** * for MixedSetter to check this setter if available */ - condition?: (field: any) => boolean; + condition?: (field: IPublicModelSettingField) => boolean; + /** * for MixedSetter to manual change to this setter */ - initialValue?: any | ((field: any) => any); + initialValue?: any | ((field: IPublicModelSettingField) => any); recommend?: boolean; // 标识是否为动态 setter,默认为 true isDynamic?: boolean; From e3a19896d78cf79736e0fabba9dd37c4c3f22997 Mon Sep 17 00:00:00 2001 From: liujuping Date: Fri, 26 Jan 2024 16:58:20 +0800 Subject: [PATCH 5/8] fix(prop): emit event when unset prop --- .../designer/src/document/node/props/prop.ts | 43 ++++--- .../tests/document/node/props/prop.test.ts | 106 +++++++++++++++++- 2 files changed, 132 insertions(+), 17 deletions(-) diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index e2d9f12e8..fd9401314 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -353,7 +353,6 @@ export class Prop implements IProp, IPropParent { @action setValue(val: IPublicTypeCompositeValue) { if (val === this._value) return; - const editor = this.owner.document?.designer.editor; const oldValue = this._value; this._value = val; this._code = null; @@ -386,22 +385,31 @@ export class Prop implements IProp, IPropParent { this.setupItems(); if (oldValue !== this._value) { - const propsInfo = { - key: this.key, - prop: this, - oldValue, - newValue: this._value, - }; - - editor?.eventBus.emit(GlobalEvent.Node.Prop.InnerChange, { - node: this.owner as any, - ...propsInfo, - }); - - this.owner?.emitPropChange?.(propsInfo); + this.emitChange({ oldValue }); } } + emitChange = ({ + oldValue, + }: { + oldValue: IPublicTypeCompositeValue | UNSET; + }) => { + const editor = this.owner.document?.designer.editor; + const propsInfo = { + key: this.key, + prop: this, + oldValue, + newValue: this.type === 'unset' ? undefined : this._value, + }; + + editor?.eventBus.emit(GlobalEvent.Node.Prop.InnerChange, { + node: this.owner as any, + ...propsInfo, + }); + + this.owner?.emitPropChange?.(propsInfo); + }; + getValue(): IPublicTypeCompositeValue { return this.export(IPublicEnumTransformStage.Serilize); } @@ -462,7 +470,12 @@ export class Prop implements IProp, IPropParent { */ @action unset() { - this._type = 'unset'; + if (this._type !== 'unset') { + this._type = 'unset'; + this.emitChange({ + oldValue: this._value, + }); + } } /** diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts index 4424eb601..8630376b6 100644 --- a/packages/designer/tests/document/node/props/prop.test.ts +++ b/packages/designer/tests/document/node/props/prop.test.ts @@ -3,7 +3,7 @@ import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; import { Designer } from '../../../../src/designer/designer'; import { DocumentModel } from '../../../../src/document/document-model'; import { Prop, isProp, isValidArrayIndex } from '../../../../src/document/node/props/prop'; -import { IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types'; import { shellModelFactory } from '../../../../../engine/src/modules/shell-model-factory'; const slotNodeImportMockFn = jest.fn(); @@ -24,9 +24,16 @@ const mockOwner = { remove: slotNodeRemoveMockFn, }; }, - designer: {}, + designer: { + editor: { + eventBus: { + emit: jest.fn(), + }, + }, + }, }, isInited: true, + emitPropChange: jest.fn(), }; const mockPropsInst = { @@ -564,3 +571,98 @@ describe('其他导出函数', () => { expect(isValidArrayIndex('2', 1)).toBeFalsy(); }); }); + +describe('setValue with event', () => { + let propInstance; + let mockEmitChange; + let mockEventBusEmit; + let mockEmitPropChange; + + beforeEach(() => { + // Initialize the instance of your class + propInstance = new Prop(mockPropsInst, true, 'stringProp');; + + // Mock necessary methods and properties + mockEmitChange = jest.spyOn(propInstance, 'emitChange'); + propInstance.owner = { + document: { + designer: { + editor: { + eventBus: { + emit: jest.fn(), + }, + }, + }, + }, + emitPropChange: jest.fn(), + }; + mockEventBusEmit = jest.spyOn(propInstance.owner.document.designer.editor.eventBus, 'emit'); + mockEmitPropChange = jest.spyOn(propInstance.owner, 'emitPropChange'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should correctly handle string values and emit changes', () => { + const oldValue = propInstance._value; + const newValue = 'new string value'; + + propInstance.setValue(newValue); + + const expectedPartialPropsInfo = expect.objectContaining({ + key: propInstance.key, + newValue, // You can specifically test only certain keys + oldValue, + }); + + expect(propInstance.getValue()).toBe(newValue); + expect(propInstance.type).toBe('literal'); + expect(mockEmitChange).toHaveBeenCalledWith({ oldValue }); + expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo); + expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo); + }); + + it('should handle object values and set type to map', () => { + const oldValue = propInstance._value; + const newValue = 234; + + const expectedPartialPropsInfo = expect.objectContaining({ + key: propInstance.key, + newValue, // You can specifically test only certain keys + oldValue, + }); + + propInstance.setValue(newValue); + + expect(propInstance.getValue()).toEqual(newValue); + expect(propInstance.type).toBe('literal'); + expect(mockEmitChange).toHaveBeenCalledWith({ oldValue }); + expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo); + expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo); + }); + + it('should has event when unset call', () => { + const oldValue = propInstance._value; + + propInstance.unset(); + + const expectedPartialPropsInfo = expect.objectContaining({ + key: propInstance.key, + newValue: undefined, // You can specifically test only certain keys + oldValue, + }); + + expect(propInstance.getValue()).toEqual(undefined); + expect(propInstance.type).toBe('unset'); + expect(mockEmitChange).toHaveBeenCalledWith({ + oldValue, + newValue: undefined, + }); + expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo); + expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo); + + propInstance.unset(); + expect(mockEmitChange).toHaveBeenCalledTimes(1); + }); +}); From ed7befbff01d174a0335f0a1832fd9e313e308ea Mon Sep 17 00:00:00 2001 From: liujuping Date: Tue, 30 Jan 2024 10:22:43 +0800 Subject: [PATCH 6/8] feat(context-menu): prevent event bubbling when "menus" is empty --- packages/shell/src/components/context-menu.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx index ccd910efc..8c7ab446b 100644 --- a/packages/shell/src/components/context-menu.tsx +++ b/packages/shell/src/components/context-menu.tsx @@ -34,7 +34,7 @@ export function ContextMenu({ children, menus, pluginContext }: { ); } - if (!menus || !menus.length) { + if (!menus) { return ( <>{ children } ); @@ -53,6 +53,9 @@ export function ContextMenu({ children, menus, pluginContext }: { } ContextMenu.create = (pluginContext: IPublicModelPluginContext, menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { pluginContext, }), { From 80bb7102b65ccfc24399e372cff5f3fbaf8258a4 Mon Sep 17 00:00:00 2001 From: liujuping Date: Tue, 30 Jan 2024 14:19:30 +0800 Subject: [PATCH 7/8] feat(command): update default commands --- .../src/inner-plugins/default-command.ts | 44 ++++++++++++++----- packages/types/src/shell/api/command.ts | 4 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/engine/src/inner-plugins/default-command.ts b/packages/engine/src/inner-plugins/default-command.ts index b2b899acb..68d65c500 100644 --- a/packages/engine/src/inner-plugins/default-command.ts +++ b/packages/engine/src/inner-plugins/default-command.ts @@ -10,10 +10,7 @@ const sampleNodeSchema: IPublicTypePropType = { value: [ { name: 'id', - propType: { - type: 'string', - isRequired: true, - }, + propType: 'string', }, { name: 'componentName', @@ -277,10 +274,12 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { handler: (param: { parentNodeId: string; nodeSchema: IPublicTypeNodeSchema; + index: number; }) => { const { parentNodeId, nodeSchema, + index, } = param; const { project } = ctx; const parentNode = project.currentDocument?.getNodeById(parentNodeId); @@ -296,7 +295,11 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { throw new Error('Invalid node.'); } - project.currentDocument?.insertNode(parentNode, nodeSchema); + if (index < 0 || index > (parentNode.children?.size || 0)) { + throw new Error(`Invalid index '${index}'.`); + } + + project.currentDocument?.insertNode(parentNode, nodeSchema, index); }, parameters: [ { @@ -309,6 +312,11 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { propType: nodeSchemaPropType, description: 'The node to be added.', }, + { + name: 'index', + propType: 'number', + description: 'The index of the node to be added.', + }, ], }); @@ -326,6 +334,14 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { index = 0, } = param; + if (!nodeId) { + throw new Error('Invalid node id.'); + } + + if (!targetNodeId) { + throw new Error('Invalid target node id.'); + } + const node = project.currentDocument?.getNodeById(nodeId); const targetNode = project.currentDocument?.getNodeById(targetNodeId); if (!node) { @@ -350,12 +366,18 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { parameters: [ { name: 'nodeId', - propType: 'string', + propType: { + type: 'string', + isRequired: true, + }, description: 'The id of the node to be moved.', }, { name: 'targetNodeId', - propType: 'string', + propType: { + type: 'string', + isRequired: true, + }, description: 'The id of the target node.', }, { @@ -393,8 +415,8 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { }); command.registerCommand({ - name: 'replace', - description: 'Replace a node with another node.', + name: 'update', + description: 'Update a node.', handler(param: { nodeId: string; nodeSchema: IPublicTypeNodeSchema; @@ -419,12 +441,12 @@ export const nodeCommand = (ctx: IPublicModelPluginContext) => { { name: 'nodeId', propType: 'string', - description: 'The id of the node to be replaced.', + description: 'The id of the node to be updated.', }, { name: 'nodeSchema', propType: nodeSchemaPropType, - description: 'The node to replace.', + description: 'The node to be updated.', }, ], }); diff --git a/packages/types/src/shell/api/command.ts b/packages/types/src/shell/api/command.ts index 5cbfaa2e1..1f8425dce 100644 --- a/packages/types/src/shell/api/command.ts +++ b/packages/types/src/shell/api/command.ts @@ -15,12 +15,12 @@ export interface IPublicApiCommand { /** * 通过名称和给定参数执行一个命令,会校验参数是否符合命令定义 */ - executeCommand(name: string, args: IPublicTypeCommandHandlerArgs): void; + executeCommand(name: string, args?: IPublicTypeCommandHandlerArgs): void; /** * 批量执行命令,执行完所有命令后再进行一次重绘,历史记录中只会记录一次 */ - batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[]): void; + batchExecuteCommand(commands: { name: string; args?: IPublicTypeCommandHandlerArgs }[]): void; /** * 列出所有已注册的命令 From 557a462b9f0e90e953862fd5fc7b528d4701a278 Mon Sep 17 00:00:00 2001 From: liujuping Date: Tue, 30 Jan 2024 14:34:53 +0800 Subject: [PATCH 8/8] fix(prop): emit event when delete prop --- .../designer/src/document/node/props/prop.ts | 1 + .../tests/document/node/props/prop.test.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index fd9401314..d70f0f56e 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -570,6 +570,7 @@ export class Prop implements IProp, IPropParent { @action remove() { this.parent.delete(this); + this.unset(); } /** diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts index 8630376b6..ff4147a34 100644 --- a/packages/designer/tests/document/node/props/prop.test.ts +++ b/packages/designer/tests/document/node/props/prop.test.ts @@ -34,11 +34,14 @@ const mockOwner = { }, isInited: true, emitPropChange: jest.fn(), + delete() {}, }; const mockPropsInst = { owner: mockOwner, + delete() {}, }; + mockPropsInst.props = mockPropsInst; describe('Prop 类测试', () => { @@ -595,6 +598,7 @@ describe('setValue with event', () => { }, }, emitPropChange: jest.fn(), + delete() {}, }; mockEventBusEmit = jest.spyOn(propInstance.owner.document.designer.editor.eventBus, 'emit'); mockEmitPropChange = jest.spyOn(propInstance.owner, 'emitPropChange'); @@ -665,4 +669,29 @@ describe('setValue with event', () => { propInstance.unset(); expect(mockEmitChange).toHaveBeenCalledTimes(1); }); + + // remove + it('should has event when remove call', () => { + const oldValue = propInstance._value; + + propInstance.remove(); + + const expectedPartialPropsInfo = expect.objectContaining({ + key: propInstance.key, + newValue: undefined, // You can specifically test only certain keys + oldValue, + }); + + expect(propInstance.getValue()).toEqual(undefined); + // expect(propInstance.type).toBe('unset'); + expect(mockEmitChange).toHaveBeenCalledWith({ + oldValue, + newValue: undefined, + }); + expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo); + expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo); + + propInstance.remove(); + expect(mockEmitChange).toHaveBeenCalledTimes(1); + }); });