diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index e2d9f12e8..d70f0f56e 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, + }); + } } /** @@ -557,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 4424eb601..ff4147a34 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,14 +24,24 @@ const mockOwner = { remove: slotNodeRemoveMockFn, }; }, - designer: {}, + designer: { + editor: { + eventBus: { + emit: jest.fn(), + }, + }, + }, }, isInited: true, + emitPropChange: jest.fn(), + delete() {}, }; const mockPropsInst = { owner: mockOwner, + delete() {}, }; + mockPropsInst.props = mockPropsInst; describe('Prop 类测试', () => { @@ -564,3 +574,124 @@ 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(), + delete() {}, + }; + 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); + }); + + // 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); + }); +}); 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/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; } 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, }), { 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; /** * 列出所有已注册的命令