diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1000fca053..670db7dfbd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,7 +2,7 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence -* @liujuping @JackLian +* @liujuping @1ncounter /modules/material-parser @akirakai /modules/code-generator @qingniaotonghua diff --git a/.github/workflows/publish engine.yml b/.github/workflows/publish engine.yml index dcbf0547e7..ddbefcde55 100644 --- a/.github/workflows/publish engine.yml +++ b/.github/workflows/publish engine.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest if: >- contains(github.ref, 'refs/heads/release/') && - (github.actor == 'JackLian' || github.actor == 'liujuping') + (github.actor == '1ncounter' || github.actor == 'liujuping') steps: - uses: actions/checkout@v2 - name: Setup Node.js diff --git a/.github/workflows/test packages.yml b/.github/workflows/test packages.yml index 4ee9b4156c..45fa665465 100644 --- a/.github/workflows/test packages.yml +++ b/.github/workflows/test packages.yml @@ -105,4 +105,36 @@ jobs: run: npm i && npm run setup:skip-build - name: test - run: cd packages/utils && npm test \ No newline at end of file + run: cd packages/utils && npm test + + test-editor-core: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: install + run: npm i && npm run setup:skip-build + + - name: test + run: cd packages/editor-core && npm test + + test-plugin-command: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: install + run: npm i && npm run setup:skip-build + + - name: test + run: cd packages/plugin-command && npm test \ No newline at end of file diff --git a/docs/docs/api/canvas.md b/docs/docs/api/canvas.md index 582f2354b5..865b9ac311 100644 --- a/docs/docs/api/canvas.md +++ b/docs/docs/api/canvas.md @@ -1,6 +1,6 @@ --- title: canvas - 画布 API -sidebar_position: 12 +sidebar_position: 10 --- > **@types** [IPublicApiCanvas](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/canvas.ts)
diff --git a/docs/docs/api/command.md b/docs/docs/api/command.md new file mode 100644 index 0000000000..fc36c8ad89 --- /dev/null +++ b/docs/docs/api/command.md @@ -0,0 +1,101 @@ +--- +title: command - 指令 API +sidebar_position: 10 +--- + + + +## 模块概览 + +该模块使得与命令系统的交互成为可能,提供了一种全面的方式来处理、执行和管理应用程序中的命令。 + + + +## 接口 + +### IPublicApiCommand + +与命令交互的接口。它提供了注册、注销、执行和管理命令的方法。 + + + +## 方法 + +### registerCommand + +注册一个新命令及其处理函数。 + +``` +typescriptCopy code +/** + * 注册一个新的命令及其处理程序。 + * @param command {IPublicTypeCommand} - 要注册的命令。 + */ +registerCommand(command: IPublicTypeCommand): void; +``` + +### unregisterCommand + +注销一个已存在的命令。 + +``` +typescriptCopy code +/** + * 注销一个已存在的命令。 + * @param name {string} - 要注销的命令的名称。 + */ +unregisterCommand(name: string): void; +``` + +### executeCommand + +根据名称和提供的参数执行命令,确保参数符合命令的定义。 + +``` +typescriptCopy code +/** + * 根据名称和提供的参数执行命令。 + * @param name {string} - 要执行的命令的名称。 + * @param args {IPublicTypeCommandHandlerArgs} - 命令的参数。 + */ +executeCommand(name: string, args?: IPublicTypeCommandHandlerArgs): void; +``` + +### batchExecuteCommand + +批量执行命令,在所有命令执行后进行重绘,历史记录中只记录一次。 + +``` +typescriptCopy code +/** + * 批量执行命令,随后进行重绘,历史记录中只记录一次。 + * @param commands {Array} - 命令对象的数组,包含名称和可选参数。 + */ +batchExecuteCommand(commands: { name: string; args?: IPublicTypeCommandHandlerArgs }[]): void; +``` + +### listCommands + +列出所有已注册的命令。 + +``` +typescriptCopy code +/** + * 列出所有已注册的命令。 + * @returns {IPublicTypeListCommand[]} - 已注册命令的数组。 + */ +listCommands(): IPublicTypeListCommand[]; +``` + +### onCommandError + +为命令执行过程中的错误注册错误处理回调函数。 + +``` +typescriptCopy code +/** + * 为命令执行过程中的错误注册一个回调函数。 + * @param callback {(name: string, error: Error) => void} - 错误处理的回调函数。 + */ +onCommandError(callback: (name: string, error: Error) => void): void; +``` diff --git a/docs/docs/api/common.md b/docs/docs/api/common.md index 6613547ea9..c278bf2ad8 100644 --- a/docs/docs/api/common.md +++ b/docs/docs/api/common.md @@ -1,6 +1,6 @@ --- title: common - 通用 API -sidebar_position: 11 +sidebar_position: 10 --- > **@types** [IPublicApiCommon](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/common.ts)
@@ -82,7 +82,7 @@ executeTransaction(fn: () => void, type: IPublicEnumTransitionType): void; ``` **@since v1.0.16** -##### 示例 +**示例** ```typescript import { common } from '@alilc/lowcode-engine'; import { IPublicEnumTransitionType } from '@alilc/lowcode-types'; @@ -132,7 +132,8 @@ createIntl(instance: string | object): { **@since v1.0.17** -##### 示例 +**示例** + ```typescript import { common } from '@alilc/lowcode-engine'; import enUS from './en-US.json'; @@ -156,7 +157,7 @@ i18n 转换方法 intl(data: IPublicTypeI18nData | string, params?: object): string; ``` -##### 示例 +**示例** ``` const title = common.utils.intl(node.title) ``` diff --git a/docs/docs/api/commonUI.md b/docs/docs/api/commonUI.md index c0bbda588e..45640051f2 100644 --- a/docs/docs/api/commonUI.md +++ b/docs/docs/api/commonUI.md @@ -1,6 +1,6 @@ --- title: commonUI - UI 组件库 -sidebar_position: 11 +sidebar_position: 10 --- ## 简介 diff --git a/docs/docs/api/config.md b/docs/docs/api/config.md index 9294b9d289..414cfc979f 100644 --- a/docs/docs/api/config.md +++ b/docs/docs/api/config.md @@ -1,6 +1,6 @@ --- title: config - 配置 API -sidebar_position: 8 +sidebar_position: 5 --- > **@types** [IPublicModelEngineConfig](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/engine-config.ts)
@@ -24,7 +24,7 @@ sidebar_position: 8 */ get(key: string, defaultValue?: any): any; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; @@ -43,7 +43,7 @@ config.get('keyB', { a: 1 }); */ set(key: string, value: any): void; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; @@ -63,7 +63,7 @@ config.set('keyC', 1); has(key: string): boolean; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; @@ -81,7 +81,7 @@ config.has('keyD'); */ setConfig(config: { [key: string]: any }): void; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; @@ -134,7 +134,7 @@ config.getPreference().set(`${panelName}-pinned-status-isFloat`, false, 'skeleto */ onceGot(key: string): Promise; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; @@ -160,7 +160,7 @@ const value = await config.onceGot('keyA'); */ onGot(key: string, fn: (data: any) => void): () => void; ``` -#### 示例 +**示例** ```typescript import { config } from '@alilc/lowcode-engine'; diff --git a/docs/docs/api/configOptions.md b/docs/docs/api/configOptions.md index 67d2fae2c1..5d6e8d7abb 100644 --- a/docs/docs/api/configOptions.md +++ b/docs/docs/api/configOptions.md @@ -1,6 +1,6 @@ --- title: config options - 配置列表 -sidebar_position: 13 +sidebar_position: 5 --- > **@types** [IPublicTypeEngineOptions](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/engine-options.ts)
diff --git a/docs/docs/api/event.md b/docs/docs/api/event.md index 0919b41fd2..c2e86f7106 100644 --- a/docs/docs/api/event.md +++ b/docs/docs/api/event.md @@ -1,6 +1,6 @@ --- title: event - 事件 API -sidebar_position: 7 +sidebar_position: 10 --- > **@types** [IPublicApiEvent](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/event.ts)
diff --git a/docs/docs/api/hotkey.md b/docs/docs/api/hotkey.md index a244b94c27..be6a3033d0 100644 --- a/docs/docs/api/hotkey.md +++ b/docs/docs/api/hotkey.md @@ -1,6 +1,6 @@ --- title: hotkey - 快捷键 API -sidebar_position: 5 +sidebar_position: 10 --- > **@types** [IPublicApiHotkey](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/hotkey.ts)
diff --git a/docs/docs/api/init.md b/docs/docs/api/init.md index 55b116a579..dd84d9c00f 100644 --- a/docs/docs/api/init.md +++ b/docs/docs/api/init.md @@ -1,6 +1,6 @@ --- title: init - 初始化 API -sidebar_position: 10 +sidebar_position: 0 --- > **@since** v1.0.0 diff --git a/docs/docs/api/logger.md b/docs/docs/api/logger.md index 7493f34dc2..38d986258b 100644 --- a/docs/docs/api/logger.md +++ b/docs/docs/api/logger.md @@ -1,6 +1,6 @@ --- title: logger - 日志 API -sidebar_position: 9 +sidebar_position: 10 --- > **@types** [IPublicApiLogger](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/logger.ts)
diff --git a/docs/docs/api/material.md b/docs/docs/api/material.md index d237180ca9..0e09441275 100644 --- a/docs/docs/api/material.md +++ b/docs/docs/api/material.md @@ -1,6 +1,6 @@ --- title: material - 物料 API -sidebar_position: 2 +sidebar_position: 10 --- > **@types** [IPublicApiMaterial](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/material.ts)
@@ -39,7 +39,7 @@ setAssets(assets: IPublicTypeAssetsJson): void; 相关类型:[IPublicTypeAssetsJson](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/assets-json.ts) -##### 示例 +**示例** 直接在项目中引用 npm 包 ```javascript import { material } from '@alilc/lowcode-engine'; @@ -85,7 +85,7 @@ getAssets(): IPublicTypeAssetsJson; 相关类型:[IPublicTypeAssetsJson](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/assets-json.ts) -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; @@ -106,7 +106,7 @@ loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise; ``` 相关类型:[IPublicTypeAssetsJson](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/assets-json.ts) -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; import assets1 from '@alilc/mc-assets-/assets.json'; @@ -146,7 +146,7 @@ addBuiltinComponentAction(action: IPublicTypeComponentAction): void; 相关类型:[IPublicTypeComponentAction](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/component-action.ts) -##### 示例 +**示例** 新增设计扩展位,并绑定事件 ```typescript import { material } from '@alilc/lowcode-engine'; @@ -186,7 +186,7 @@ removeBuiltinComponentAction(name: string): void; - lock:锁定,不可编辑 - unlock:解锁,可编辑 -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; @@ -222,7 +222,7 @@ modifyBuiltinComponentAction( -##### 示例 +**示例** 给原始的 remove 扩展时间添加执行前后的日志 ```typescript import { material } from '@alilc/lowcode-engine'; @@ -335,7 +335,7 @@ getComponentMeta(componentName: string): IPublicModelComponentMeta | null; ``` 相关类型:[IPublicModelComponentMeta](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/component-meta.ts) -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; @@ -356,7 +356,7 @@ material.getComponentMeta('Input'); ``` 相关类型:[IPublicModelComponentMeta](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/component-meta.ts) -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; @@ -393,7 +393,7 @@ registerMetadataTransducer( ): void; ``` -##### 示例 +**示例** 给每一个组件的配置添加高级配置面板,其中有一个是否渲染配置项 ```typescript import { material } from '@alilc/lowcode-engine' @@ -475,7 +475,7 @@ material.registerMetadataTransducer((transducer) => { getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[]; ``` -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine' @@ -496,7 +496,7 @@ onChangeAssets(fn: () => void): IPublicTypeDisposable; 相关类型:[IPublicTypeDisposable](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/disposable.ts) -##### 示例 +**示例** ```typescript import { material } from '@alilc/lowcode-engine'; diff --git a/docs/docs/api/model/_category_.json b/docs/docs/api/model/_category_.json index 5b1f74b36f..3afc4f79b5 100644 --- a/docs/docs/api/model/_category_.json +++ b/docs/docs/api/model/_category_.json @@ -1,6 +1,6 @@ { "label": "模型定义 Models", - "position": 14, + "position": 100, "collapsed": false, "collapsible": true } diff --git a/docs/docs/api/plugins.md b/docs/docs/api/plugins.md index e35411d3a8..df025f49e9 100644 --- a/docs/docs/api/plugins.md +++ b/docs/docs/api/plugins.md @@ -1,6 +1,6 @@ --- title: plugins - 插件 API -sidebar_position: 4 +sidebar_position: 2 --- > **@types** [IPublicApiPlugins](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/plugins.ts)
> **@since** v1.0.0 diff --git a/docs/docs/api/project.md b/docs/docs/api/project.md index 7998228e2b..54bd1474cf 100644 --- a/docs/docs/api/project.md +++ b/docs/docs/api/project.md @@ -1,6 +1,6 @@ --- title: project - 模型 API -sidebar_position: 3 +sidebar_position: 10 --- ## 模块简介 @@ -201,7 +201,7 @@ addPropsTransducer( - [IPublicTypePropsTransducer](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/props-transducer.ts) - [IPublicEnumTransformStage](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/enum/transform-stage.ts) -#### 示例 +**示例** 在保存的时候删除每一个组件的 props.hidden ```typescript import { project } from '@alilc/lowcode-engine'; diff --git a/docs/docs/api/setters.md b/docs/docs/api/setters.md index cc7b6d429c..0d3435b3d3 100644 --- a/docs/docs/api/setters.md +++ b/docs/docs/api/setters.md @@ -1,6 +1,6 @@ --- title: setters - 设置器 API -sidebar_position: 6 +sidebar_position: 10 --- > **@types** [IPublicApiSetters](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/setters.ts)
> **@since** v1.0.0 diff --git a/docs/docs/api/simulatorHost.md b/docs/docs/api/simulatorHost.md index ee8f038fc3..70eaca0220 100644 --- a/docs/docs/api/simulatorHost.md +++ b/docs/docs/api/simulatorHost.md @@ -1,6 +1,6 @@ --- title: simulatorHost - 模拟器 API -sidebar_position: 3 +sidebar_position: 10 --- > **@types** [IPublicApiSimulatorHost](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/simulator-host.ts)
> **@since** v1.0.0 @@ -20,7 +20,7 @@ sidebar_position: 3 */ set(key: string, value: any): void; ``` -#### 示例 +**示例** 设置若干用于画布渲染的变量,比如画布大小、locale 等。 以设置画布大小为例: diff --git a/docs/docs/api/skeleton.md b/docs/docs/api/skeleton.md index 0713546e19..396fad9e9e 100644 --- a/docs/docs/api/skeleton.md +++ b/docs/docs/api/skeleton.md @@ -1,6 +1,6 @@ --- title: skeleton - 面板 API -sidebar_position: 1 +sidebar_position: 10 --- > **@types** [IPublicApiSkeleton](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/api/skeleton.ts)
> **@since** v1.0.0 diff --git a/docs/docs/api/workspace.md b/docs/docs/api/workspace.md index 6d0714ae09..74f7d6950f 100644 --- a/docs/docs/api/workspace.md +++ b/docs/docs/api/workspace.md @@ -1,6 +1,6 @@ --- title: workspace - 应用级 API -sidebar_position: 12 +sidebar_position: 10 --- > **[@experimental](./#experimental)**
diff --git a/docs/docs/demoUsage/makeStuff/dialog.md b/docs/docs/demoUsage/makeStuff/dialog.md index 56303067cb..da78cc8e8c 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) diff --git a/docs/package.json b/docs/package.json index 3b7f1e95c2..7facd9db8b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine-docs", - "version": "1.2.28", + "version": "1.2.31", "description": "低代码引擎版本化文档", "license": "MIT", "files": [ diff --git a/lerna.json b/lerna.json index 7de339c6cd..7fad993f66 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "4.0.0", - "version": "1.3.1", + "version": "1.3.2", "npmClient": "yarn", "useWorkspaces": true, "packages": [ diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js index f0ad2e861e..3684a48acb 100644 --- a/packages/designer/jest.config.js +++ b/packages/designer/jest.config.js @@ -21,6 +21,7 @@ const jestConfig = { // testMatch: ['**/builtin-hotkey.test.ts'], // testMatch: ['**/selection.test.ts'], // testMatch: ['**/plugin/sequencify.test.ts'], + // testMatch: ['**/builtin-simulator/utils/parse-metadata.test.ts'], transformIgnorePatterns: [ `/node_modules/(?!${esModules})/`, ], diff --git a/packages/designer/package.json b/packages/designer/package.json index ec7c153c80..97256d3a21 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-designer", - "version": "1.3.1", + "version": "1.3.2", "description": "Designer for Ali LowCode Engine", "main": "lib/index.js", "module": "es/index.js", @@ -15,9 +15,9 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "react": "^16", "react-dom": "^16.7.0", diff --git a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts b/packages/designer/src/builtin-simulator/utils/parse-metadata.ts index 5c81340a14..6969a47db5 100644 --- a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts +++ b/packages/designer/src/builtin-simulator/utils/parse-metadata.ts @@ -16,8 +16,16 @@ export const primitiveTypes = [ 'any', ]; +interface LowcodeCheckType { + // isRequired, props, propName, componentName, location, propFullName, secret + (props: any, propName: string, componentName: string, ...rest: any[]): Error | null; + // (...reset: any[]): Error | null; + isRequired?: LowcodeCheckType; + type?: string | object; +} + // eslint-disable-next-line @typescript-eslint/ban-types -function makeRequired(propType: any, lowcodeType: string | object) { +function makeRequired(propType: any, lowcodeType: string | object): LowcodeCheckType { function lowcodeCheckTypeIsRequired(...rest: any[]) { return propType.isRequired(...rest); } @@ -34,7 +42,7 @@ function makeRequired(propType: any, lowcodeType: string | object) { } // eslint-disable-next-line @typescript-eslint/ban-types -function define(propType: any = PropTypes.any, lowcodeType: string | object = {}) { +function define(propType: any = PropTypes.any, lowcodeType: string | object = {}): LowcodeCheckType { if (!propType._inner && propType.name !== 'lowcodeCheckType') { propType.lowcodeType = lowcodeType; } diff --git a/packages/designer/src/designer/setting/utils.ts b/packages/designer/src/designer/setting/utils.ts index 1e061f20ae..75ed1dfc1a 100644 --- a/packages/designer/src/designer/setting/utils.ts +++ b/packages/designer/src/designer/setting/utils.ts @@ -70,7 +70,7 @@ export class Transducer { } if (isDynamicSetter(setter) && isDynamic) { try { - setter = setter.call(context, context); + setter = setter.call(context.internalToShellField(), context.internalToShellField()); } catch (e) { console.error(e); } } diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index e2d9f12e8e..d70f0f56ec 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/src/plugin/plugin-types.ts b/packages/designer/src/plugin/plugin-types.ts index d648067a36..cfc38866f5 100644 --- a/packages/designer/src/plugin/plugin-types.ts +++ b/packages/designer/src/plugin/plugin-types.ts @@ -19,6 +19,7 @@ import { IPublicModelWindow, IPublicEnumPluginRegisterLevel, IPublicApiCommonUI, + IPublicApiCommand, } from '@alilc/lowcode-types'; import PluginContext from './plugin-context'; @@ -63,6 +64,7 @@ export interface ILowCodePluginContextPrivate { set registerLevel(level: IPublicEnumPluginRegisterLevel); set isPluginRegisteredInWorkspace(flag: boolean); set commonUI(commonUI: IPublicApiCommonUI); + set command(command: IPublicApiCommand); } export interface ILowCodePluginContextApiAssembler { assembleApis( diff --git a/packages/designer/tests/builtin-simulator/utils/parse-metadata.test.ts b/packages/designer/tests/builtin-simulator/utils/parse-metadata.test.ts index 1843a24429..64e19376e2 100644 --- a/packages/designer/tests/builtin-simulator/utils/parse-metadata.test.ts +++ b/packages/designer/tests/builtin-simulator/utils/parse-metadata.test.ts @@ -1,5 +1,7 @@ import '../../fixtures/window'; -import { parseMetadata } from '../../../src/builtin-simulator/utils/parse-metadata'; +import PropTypes from 'prop-types'; +import { LowcodeTypes, parseMetadata, parseProps } from '../../../src/builtin-simulator/utils/parse-metadata'; +import { default as ReactPropTypesSecret } from 'prop-types/lib/ReactPropTypesSecret'; describe('parseMetadata', () => { it('parseMetadata', async () => { @@ -11,3 +13,165 @@ describe('parseMetadata', () => { expect(result).toBeDefined(); }); }); + +describe('LowcodeTypes basic type validators', () => { + it('should validate string types', () => { + const stringValidator = LowcodeTypes.string; + // 对 stringValidator 进行测试 + const props = { testProp: 'This is a string' }; + const propName = 'testProp'; + const componentName = 'TestComponent'; + + const result = stringValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeNull(); // No error for valid string + }); + + it('should fail with a non-string type', () => { + const stringValidator = LowcodeTypes.string; + const props = { testProp: 42 }; + const propName = 'testProp'; + const componentName = 'TestComponent'; + + const result = stringValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeInstanceOf(Error); // Error for non-string type + expect(result.message).toContain('Invalid prop `testProp` of type `number` supplied to `TestComponent`, expected `string`.'); + }); + + it('should pass with a valid number', () => { + const numberValidator = LowcodeTypes.number; + const props = { testProp: 42 }; + const propName = 'testProp'; + const componentName = 'TestComponent'; + + const result = numberValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeNull(); // No error for valid number + }); + + it('should fail with a non-number type', () => { + const numberValidator = LowcodeTypes.number; + const props = { testProp: 'Not a number' }; + const propName = 'testProp'; + const componentName = 'TestComponent'; + + const result = numberValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeInstanceOf(Error); // Error for non-number type + expect(result.message).toContain('Invalid prop `testProp` of type `string` supplied to `TestComponent`, expected `number`.'); + }); +}); + +describe('Custom type constructors', () => { + it('should create a custom type validator using define', () => { + const customType = LowcodeTypes.define(PropTypes.string, 'customType'); + const props = { testProp: 'This is a string' }; + const propName = 'testProp'; + const componentName = 'TestComponent'; + + // 测试有效值 + const validResult = customType(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(validResult).toBeNull(); // No error for valid string + + // 测试无效值 + const invalidProps = { testProp: 42 }; + const invalidResult = customType(invalidProps, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(invalidResult).toBeInstanceOf(Error); // Error for non-string type + + // 验证 lowcodeType 属性 + expect(customType.lowcodeType).toEqual('customType'); + + // 验证 isRequired 属性 + const requiredResult = customType.isRequired(invalidProps, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(requiredResult).toBeInstanceOf(Error); // Error for non-string type + }); +}); + + +describe('Advanced type constructors', () => { + describe('oneOf Type Validator', () => { + const oneOfValidator = LowcodeTypes.oneOf(['red', 'green', 'blue']); + const propName = 'color'; + const componentName = 'ColorPicker'; + + it('should pass with a valid value', () => { + const props = { color: 'red' }; + const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeNull(); // No error for valid value + }); + + it('should fail with an invalid value', () => { + const props = { color: 'yellow' }; + const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeInstanceOf(Error); // Error for invalid value + expect(result.message).toContain(`Invalid prop \`${propName}\` of value \`yellow\` supplied to \`${componentName}\`, expected one of ["red","green","blue"].`); + }); + + it('should fail with a non-existing value', () => { + const props = { color: 'others' }; + const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret); + expect(result).toBeInstanceOf(Error); // Error for non-existing value + expect(result.message).toContain(`Invalid prop \`${propName}\` of value \`others\` supplied to \`${componentName}\`, expected one of ["red","green","blue"].`); + }); + }); +}); + + +describe('parseProps function', () => { + it('should correctly parse propTypes and defaultProps', () => { + const component = { + propTypes: { + name: LowcodeTypes.string, + age: LowcodeTypes.number, + }, + defaultProps: { + name: 'John Doe', + age: 30, + }, + }; + const parsedProps = parseProps(component); + + // 测试结果长度 + expect(parsedProps.length).toBe(2); + + // 测试 name 属性 + const nameProp: any = parsedProps.find(prop => prop.name === 'name'); + expect(nameProp).toBeDefined(); + expect(nameProp.propType).toEqual('string'); + expect(nameProp.defaultValue).toEqual('John Doe'); + + // 测试 age 属性 + const ageProp: any = parsedProps.find(prop => prop.name === 'age'); + expect(ageProp).toBeDefined(); + expect(ageProp.propType).toEqual('number'); + expect(ageProp.defaultValue).toEqual(30); + }); +}); + +describe('parseProps function', () => { + it('should correctly parse propTypes and defaultProps', () => { + const component = { + propTypes: { + name: LowcodeTypes.string, + age: LowcodeTypes.number, + }, + defaultProps: { + name: 'John Doe', + age: 30, + }, + }; + const parsedProps = parseProps(component); + + // 测试结果长度 + expect(parsedProps.length).toBe(2); + + // 测试 name 属性 + const nameProp: any = parsedProps.find(prop => prop.name === 'name'); + expect(nameProp).toBeDefined(); + expect(nameProp.propType).toEqual('string'); + expect(nameProp.defaultValue).toEqual('John Doe'); + + // 测试 age 属性 + const ageProp: any = parsedProps.find(prop => prop.name === 'age'); + expect(ageProp).toBeDefined(); + expect(ageProp.propType).toEqual('number'); + expect(ageProp.defaultValue).toEqual(30); + }); +}); diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts index 4424eb6010..ff4147a34a 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/editor-core/build.test.json b/packages/editor-core/build.test.json new file mode 100644 index 0000000000..10d18109b8 --- /dev/null +++ b/packages/editor-core/build.test.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + "@alilc/build-plugin-lce", + "@alilc/lowcode-test-mate/plugin/index.ts" + ], + "babelPlugins": [ + ["@babel/plugin-proposal-private-property-in-object", { "loose": true }] + ] +} diff --git a/packages/editor-core/jest.config.js b/packages/editor-core/jest.config.js new file mode 100644 index 0000000000..e8441e3dbb --- /dev/null +++ b/packages/editor-core/jest.config.js @@ -0,0 +1,26 @@ +const fs = require('fs'); +const { join } = require('path'); +const esModules = [].join('|'); +const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); + +const jestConfig = { + transformIgnorePatterns: [ + `/node_modules/(?!${esModules})/`, + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/icons/**', + '!src/locale/**', + '!**/node_modules/**', + '!**/vendor/**', + ], +}; + +// 只对本仓库内的 pkg 做 mapping +jestConfig.moduleNameMapper = {}; +jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index f807ef5155..55f6d50c39 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-editor-core", - "version": "1.3.1", + "version": "1.3.2", "description": "Core Api for Ali lowCode engine", "license": "MIT", "main": "lib/index.js", @@ -10,12 +10,14 @@ "es" ], "scripts": { - "build": "build-scripts build" + "build": "build-scripts build", + "test": "build-scripts test --config build.test.json", + "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "debug": "^4.1.1", "intl-messageformat": "^9.3.1", diff --git a/packages/editor-core/src/command.ts b/packages/editor-core/src/command.ts new file mode 100644 index 0000000000..7facc33d94 --- /dev/null +++ b/packages/editor-core/src/command.ts @@ -0,0 +1,91 @@ +import { IPublicApiCommand, IPublicEnumTransitionType, IPublicModelPluginContext, IPublicTypeCommand, IPublicTypeCommandHandlerArgs, IPublicTypeListCommand } from '@alilc/lowcode-types'; +import { checkPropTypes } from '@alilc/lowcode-utils'; +export interface ICommand extends Omit { + registerCommand(command: IPublicTypeCommand, options?: { + commandScope?: string; + }): void; + + batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[], pluginContext?: IPublicModelPluginContext): void; +} + +export interface ICommandOptions { + commandScope?: string; +} + +export class Command implements ICommand { + private commands: Map = new Map(); + private commandErrors: Function[] = []; + + registerCommand(command: IPublicTypeCommand, options?: ICommandOptions): void { + if (!options?.commandScope) { + throw new Error('plugin meta.commandScope is required.'); + } + const name = `${options.commandScope}:${command.name}`; + if (this.commands.has(name)) { + throw new Error(`Command '${command.name}' is already registered.`); + } + this.commands.set(name, { + ...command, + name, + }); + } + + unregisterCommand(name: string): void { + if (!this.commands.has(name)) { + throw new Error(`Command '${name}' is not registered.`); + } + this.commands.delete(name); + } + + executeCommand(name: string, args: IPublicTypeCommandHandlerArgs): void { + const command = this.commands.get(name); + if (!command) { + throw new Error(`Command '${name}' is not registered.`); + } + command.parameters?.forEach(d => { + if (!checkPropTypes(args[d.name], d.name, d.propType, 'command')) { + throw new Error(`Command '${name}' arguments ${d.name} is invalid.`); + } + }); + try { + command.handler(args); + } catch (error) { + if (this.commandErrors && this.commandErrors.length) { + this.commandErrors.forEach(callback => callback(name, error)); + } else { + throw error; + } + } + } + + batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[], pluginContext: IPublicModelPluginContext): void { + if (!commands || !commands.length) { + return; + } + pluginContext.common.utils.executeTransaction(() => { + commands.forEach(command => this.executeCommand(command.name, command.args)); + }, IPublicEnumTransitionType.REPAINT); + } + + listCommands(): IPublicTypeListCommand[] { + return Array.from(this.commands.values()).map(d => { + const result: IPublicTypeListCommand = { + name: d.name, + }; + + if (d.description) { + result.description = d.description; + } + + if (d.parameters) { + result.parameters = d.parameters; + } + + return result; + }); + } + + onCommandError(callback: (name: string, error: Error) => void): void { + this.commandErrors.push(callback); + } +} diff --git a/packages/editor-core/src/di/setter.ts b/packages/editor-core/src/di/setter.ts index 437d9a89e2..5af2c0230f 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-core/src/index.ts b/packages/editor-core/src/index.ts index 9b0542bdaf..c4a54bccde 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -6,3 +6,4 @@ export * from './hotkey'; export * from './widgets'; export * from './config'; export * from './event-bus'; +export * from './command'; diff --git a/packages/editor-core/test/command.test.ts b/packages/editor-core/test/command.test.ts new file mode 100644 index 0000000000..bb2e15943d --- /dev/null +++ b/packages/editor-core/test/command.test.ts @@ -0,0 +1,326 @@ +import { Command } from '../src/command'; + +describe('Command', () => { + let commandInstance; + let mockHandler; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + }); + + describe('registerCommand', () => { + it('should register a command successfully', () => { + const command = { + name: 'testCommand', + handler: mockHandler, + }; + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + + const registeredCommand = commandInstance.listCommands().find(c => c.name === 'testScope:testCommand'); + expect(registeredCommand).toBeDefined(); + expect(registeredCommand.name).toBe('testScope:testCommand'); + }); + + it('should throw an error if commandScope is not provided', () => { + const command = { + name: 'testCommand', + handler: mockHandler, + }; + + expect(() => { + commandInstance.registerCommand(command); + }).toThrow('plugin meta.commandScope is required.'); + }); + + it('should throw an error if command is already registered', () => { + const command = { + name: 'testCommand', + handler: mockHandler, + }; + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + + expect(() => { + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + }).toThrow(`Command 'testCommand' is already registered.`); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); + +describe('unregisterCommand', () => { + let commandInstance; + let mockHandler; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + // 先注册一个命令以便之后注销 + const command = { + name: 'testCommand', + handler: mockHandler, + }; + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + }); + + it('should unregister a command successfully', () => { + const commandName = 'testScope:testCommand'; + expect(commandInstance.listCommands().find(c => c.name === commandName)).toBeDefined(); + + commandInstance.unregisterCommand(commandName); + + expect(commandInstance.listCommands().find(c => c.name === commandName)).toBeUndefined(); + }); + + it('should throw an error if the command is not registered', () => { + const nonExistingCommandName = 'testScope:nonExistingCommand'; + expect(() => { + commandInstance.unregisterCommand(nonExistingCommandName); + }).toThrow(`Command '${nonExistingCommandName}' is not registered.`); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); + +describe('executeCommand', () => { + let commandInstance; + let mockHandler; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + // 注册一个带参数校验的命令 + const command = { + name: 'testCommand', + handler: mockHandler, + parameters: [ + { name: 'param1', propType: 'string' }, + { name: 'param2', propType: 'number' } + ], + }; + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + }); + + it('should execute a command successfully', () => { + const commandName = 'testScope:testCommand'; + const args = { param1: 'test', param2: 42 }; + + commandInstance.executeCommand(commandName, args); + + expect(mockHandler).toHaveBeenCalledWith(args); + }); + + it('should throw an error if the command is not registered', () => { + const nonExistingCommandName = 'testScope:nonExistingCommand'; + expect(() => { + commandInstance.executeCommand(nonExistingCommandName, {}); + }).toThrow(`Command '${nonExistingCommandName}' is not registered.`); + }); + + it('should throw an error if arguments are invalid', () => { + const commandName = 'testScope:testCommand'; + const invalidArgs = { param1: 'test', param2: 'not-a-number' }; // param2 should be a number + + expect(() => { + commandInstance.executeCommand(commandName, invalidArgs); + }).toThrow(`Command '${commandName}' arguments param2 is invalid.`); + }); + + it('should handle errors thrown by the command handler', () => { + const commandName = 'testScope:testCommand'; + const args = { param1: 'test', param2: 42 }; + const errorMessage = 'Command handler error'; + mockHandler.mockImplementation(() => { + throw new Error(errorMessage); + }); + + expect(() => { + commandInstance.executeCommand(commandName, args); + }).toThrow(errorMessage); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); + +describe('batchExecuteCommand', () => { + let commandInstance; + let mockHandler; + let mockExecuteTransaction; + let mockPluginContext; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + mockExecuteTransaction = jest.fn(callback => callback()); + mockPluginContext = { + common: { + utils: { + executeTransaction: mockExecuteTransaction + } + } + }; + + // 注册几个命令 + const command1 = { + name: 'testCommand1', + handler: mockHandler, + }; + const command2 = { + name: 'testCommand2', + handler: mockHandler, + }; + commandInstance.registerCommand(command1, { commandScope: 'testScope' }); + commandInstance.registerCommand(command2, { commandScope: 'testScope' }); + }); + + it('should execute a batch of commands', () => { + const commands = [ + { name: 'testScope:testCommand1', args: { param: 'value1' } }, + { name: 'testScope:testCommand2', args: { param: 'value2' } }, + ]; + + commandInstance.batchExecuteCommand(commands, mockPluginContext); + + expect(mockExecuteTransaction).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith({ param: 'value1' }); + expect(mockHandler).toHaveBeenCalledWith({ param: 'value2' }); + }); + + it('should not execute anything if commands array is empty', () => { + commandInstance.batchExecuteCommand([], mockPluginContext); + + expect(mockExecuteTransaction).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('should handle errors thrown during command execution', () => { + const errorMessage = 'Command handler error'; + mockHandler.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const commands = [ + { name: 'testScope:testCommand1', args: { param: 'value1' } }, + { name: 'testScope:testCommand2', args: { param: 'value2' } }, + ]; + + expect(() => { + commandInstance.batchExecuteCommand(commands, mockPluginContext); + }).toThrow(errorMessage); + + expect(mockExecuteTransaction).toHaveBeenCalledTimes(1); // Still called once + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); + +describe('listCommands', () => { + let commandInstance; + let mockHandler; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + }); + + it('should list all registered commands', () => { + // 注册几个命令 + const command1 = { + name: 'testCommand1', + handler: mockHandler, + description: 'Test Command 1', + parameters: [{ name: 'param1', propType: 'string' }] + }; + const command2 = { + name: 'testCommand2', + handler: mockHandler, + description: 'Test Command 2', + parameters: [{ name: 'param2', propType: 'number' }] + }; + commandInstance.registerCommand(command1, { commandScope: 'testScope' }); + commandInstance.registerCommand(command2, { commandScope: 'testScope' }); + + const listedCommands = commandInstance.listCommands(); + + expect(listedCommands.length).toBe(2); + expect(listedCommands).toEqual(expect.arrayContaining([ + expect.objectContaining({ + name: 'testScope:testCommand1', + description: 'Test Command 1', + parameters: [{ name: 'param1', propType: 'string' }] + }), + expect.objectContaining({ + name: 'testScope:testCommand2', + description: 'Test Command 2', + parameters: [{ name: 'param2', propType: 'number' }] + }) + ])); + }); + + it('should return an empty array if no commands are registered', () => { + const listedCommands = commandInstance.listCommands(); + expect(listedCommands).toEqual([]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); + +describe('onCommandError', () => { + let commandInstance; + let mockHandler; + let mockErrorHandler1; + let mockErrorHandler2; + + beforeEach(() => { + commandInstance = new Command(); + mockHandler = jest.fn(); + mockErrorHandler1 = jest.fn(); + mockErrorHandler2 = jest.fn(); + + // 注册一个命令,该命令会抛出错误 + const command = { + name: 'testCommand', + handler: () => { + throw new Error('Command execution failed'); + }, + }; + commandInstance.registerCommand(command, { commandScope: 'testScope' }); + }); + + it('should call all registered error handlers when a command throws an error', () => { + const commandName = 'testScope:testCommand'; + commandInstance.onCommandError(mockErrorHandler1); + commandInstance.onCommandError(mockErrorHandler2); + + expect(() => { + commandInstance.executeCommand(commandName, {}); + }).not.toThrow(); + + // 确保所有错误处理函数都被调用,并且传递了正确的参数 + expect(mockErrorHandler1).toHaveBeenCalledWith(commandName, expect.any(Error)); + expect(mockErrorHandler2).toHaveBeenCalledWith(commandName, expect.any(Error)); + }); + + it('should throw the error if no error handlers are registered', () => { + const commandName = 'testScope:testCommand'; + + expect(() => { + commandInstance.executeCommand(commandName, {}); + }).toThrow('Command execution failed'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); diff --git a/packages/editor-skeleton/package.json b/packages/editor-skeleton/package.json index 7c14943552..63aab7e48b 100644 --- a/packages/editor-skeleton/package.json +++ b/packages/editor-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-editor-skeleton", - "version": "1.3.1", + "version": "1.3.2", "description": "alibaba lowcode editor skeleton", "main": "lib/index.js", "module": "es/index.js", @@ -19,10 +19,10 @@ ], "dependencies": { "@alifd/next": "^1.20.12", - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "react": "^16.8.1", "react-dom": "^16.8.1" diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-pane.tsx index 1d651bb5a3..1561bf8bbe 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 d22f07f437..573d24ac62 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/engine/package.json b/packages/engine/package.json index 4b622eaa4b..23b3521213 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine", - "version": "1.3.1", + "version": "1.3.2", "description": "An enterprise-class low-code technology stack with scale-out design / 一套面向扩展设计的企业级低代码技术体系", "main": "lib/engine-core.js", "module": "es/engine-core.js", @@ -19,15 +19,16 @@ "license": "MIT", "dependencies": { "@alifd/next": "^1.19.12", - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-editor-skeleton": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-editor-skeleton": "1.3.2", "@alilc/lowcode-engine-ext": "^1.0.0", - "@alilc/lowcode-plugin-designer": "1.3.1", - "@alilc/lowcode-plugin-outline-pane": "1.3.1", - "@alilc/lowcode-shell": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", - "@alilc/lowcode-workspace": "1.3.1", + "@alilc/lowcode-plugin-command": "1.3.2", + "@alilc/lowcode-plugin-designer": "1.3.2", + "@alilc/lowcode-plugin-outline-pane": "1.3.2", + "@alilc/lowcode-shell": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", + "@alilc/lowcode-workspace": "1.3.2", "react": "^16.8.1", "react-dom": "^16.8.1" }, diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index 3ccfdf51d1..4dffa628bd 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -10,6 +10,7 @@ import { Setters as InnerSetters, Hotkey as InnerHotkey, IEditor, + Command as InnerCommand, } from '@alilc/lowcode-editor-core'; import { IPublicTypeEngineOptions, @@ -19,6 +20,7 @@ import { IPublicApiPlugins, IPublicApiWorkspace, IPublicEnumPluginRegisterLevel, + IPublicModelPluginContext, } from '@alilc/lowcode-types'; import { Designer, @@ -52,6 +54,7 @@ import { Workspace, Config, CommonUI, + Command, } from '@alilc/lowcode-shell'; import { isPlainObject } from '@alilc/lowcode-utils'; import './modules/live-editing'; @@ -63,6 +66,7 @@ import { defaultPanelRegistry } from './inner-plugins/default-panel-registry'; import { shellModelFactory } from './modules/shell-model-factory'; import { builtinHotkey } from './inner-plugins/builtin-hotkey'; import { defaultContextMenu } from './inner-plugins/default-context-menu'; +import { CommandPlugin } from '@alilc/lowcode-plugin-command'; import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane'; export * from './modules/skeleton-types'; @@ -80,6 +84,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins await plugins.register(builtinHotkey); await plugins.register(registerDefaults, {}, { autoInit: true }); await plugins.register(defaultContextMenu); + await plugins.register(CommandPlugin, {}); return () => { plugins.delete(OutlinePlugin.pluginName); @@ -89,6 +94,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins plugins.delete(builtinHotkey.pluginName); plugins.delete(registerDefaults.pluginName); plugins.delete(defaultContextMenu.pluginName); + plugins.delete(CommandPlugin.pluginName); }; } @@ -99,6 +105,8 @@ globalContext.register(editor, Editor); globalContext.register(editor, 'editor'); globalContext.register(innerWorkspace, 'workspace'); +const engineContext: Partial = {}; + const innerSkeleton = new InnerSkeleton(editor); editor.set('skeleton' as any, innerSkeleton); @@ -113,6 +121,8 @@ const project = new Project(innerProject); const skeleton = new Skeleton(innerSkeleton, 'any', false); const innerSetters = new InnerSetters(); const setters = new Setters(innerSetters); +const innerCommand = new InnerCommand(); +const command = new Command(innerCommand, engineContext as IPublicModelPluginContext); const material = new Material(editor); const commonUI = new CommonUI(editor); @@ -136,6 +146,7 @@ const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { context.setters = setters; context.material = material; const eventPrefix = meta?.eventPrefix || 'common'; + const commandScope = meta?.commandScope; context.event = new Event(commonEvent, { prefix: eventPrefix }); context.config = config; context.common = common; @@ -144,6 +155,9 @@ const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); context.workspace = workspace; context.commonUI = commonUI; + context.command = new Command(innerCommand, context as IPublicModelPluginContext, { + commandScope, + }); context.registerLevel = IPublicEnumPluginRegisterLevel.Default; context.isPluginRegisteredInWorkspace = false; editor.set('pluginContext', context); @@ -155,6 +169,20 @@ plugins = new Plugins(innerPlugins).toProxy(); editor.set('innerPlugins' as any, innerPlugins); editor.set('plugins' as any, plugins); +engineContext.skeleton = skeleton; +engineContext.plugins = plugins; +engineContext.project = project; +engineContext.setters = setters; +engineContext.material = material; +engineContext.event = event; +engineContext.logger = logger; +engineContext.hotkey = hotkey; +engineContext.common = common; +engineContext.workspace = workspace; +engineContext.canvas = canvas; +engineContext.commonUI = commonUI; +engineContext.command = command; + export { skeleton, plugins, @@ -169,6 +197,7 @@ export { workspace, canvas, commonUI, + command, }; // declare this is open-source version export const isOpenSource = true; diff --git a/packages/ignitor/package.json b/packages/ignitor/package.json index a32e30783f..0b109a7ad7 100644 --- a/packages/ignitor/package.json +++ b/packages/ignitor/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-ignitor", - "version": "1.3.1", + "version": "1.3.2", "description": "点火器,bootstrap lce project", "main": "lib/index.js", "private": true, diff --git a/packages/plugin-command/README.md b/packages/plugin-command/README.md new file mode 100644 index 0000000000..8476b47e55 --- /dev/null +++ b/packages/plugin-command/README.md @@ -0,0 +1,11 @@ +# `@alilc/plugin-command` + +> TODO: description + +## Usage + +``` +const pluginCommand = require('@alilc/plugin-command'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/plugin-command/__tests__/node-command.test.ts b/packages/plugin-command/__tests__/node-command.test.ts new file mode 100644 index 0000000000..2e9d21b35e --- /dev/null +++ b/packages/plugin-command/__tests__/node-command.test.ts @@ -0,0 +1,110 @@ +import { checkPropTypes } from '@alilc/lowcode-utils/src/check-prop-types'; +import { nodeSchemaPropType } from '../src/node-command'; + +describe('nodeSchemaPropType', () => { + const componentName = 'NodeComponent'; + const getPropType = (name: string) => nodeSchemaPropType.value.find(d => d.name === name)?.propType; + + it('should validate the id as a string', () => { + const validId = 'node1'; + const invalidId = 123; // Not a string + expect(checkPropTypes(validId, 'id', getPropType('id'), componentName)).toBe(true); + expect(checkPropTypes(invalidId, 'id', getPropType('id'), componentName)).toBe(false); + // is not required + expect(checkPropTypes(undefined, 'id', getPropType('id'), componentName)).toBe(true); + }); + + it('should validate the componentName as a string', () => { + const validComponentName = 'Button'; + const invalidComponentName = false; // Not a string + expect(checkPropTypes(validComponentName, 'componentName', getPropType('componentName'), componentName)).toBe(true); + expect(checkPropTypes(invalidComponentName, 'componentName', getPropType('componentName'), componentName)).toBe(false); + // isRequired + expect(checkPropTypes(undefined, 'componentName', getPropType('componentName'), componentName)).toBe(false); + }); + + it('should validate the props as an object', () => { + const validProps = { key: 'value' }; + const invalidProps = 'Not an object'; // Not an object + expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); + expect(checkPropTypes(invalidProps, 'props', getPropType('props'), componentName)).toBe(false); + }); + + it('should validate the props as a JSExpression', () => { + const validProps = { type: 'JSExpression', value: 'props' }; + expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); + }); + + it('should validate the props as a JSFunction', () => { + const validProps = { type: 'JSFunction', value: 'props' }; + expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); + }); + + it('should validate the props as a JSSlot', () => { + const validProps = { type: 'JSSlot', value: 'props' }; + expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); + }); + + it('should validate the condition as a bool', () => { + const validCondition = true; + const invalidCondition = 'Not a bool'; // Not a boolean + expect(checkPropTypes(validCondition, 'condition', getPropType('condition'), componentName)).toBe(true); + expect(checkPropTypes(invalidCondition, 'condition', getPropType('condition'), componentName)).toBe(false); + }); + + it('should validate the condition as a JSExpression', () => { + const validCondition = { type: 'JSExpression', value: '1 + 1 === 2' }; + const invalidCondition = { type: 'JSExpression', value: 123 }; // Not a string + expect(checkPropTypes(validCondition, 'condition', getPropType('condition'), componentName)).toBe(true); + expect(checkPropTypes(invalidCondition, 'condition', getPropType('condition'), componentName)).toBe(false); + }); + + it('should validate the loop as an array', () => { + const validLoop = ['item1', 'item2']; + const invalidLoop = 'Not an array'; // Not an array + expect(checkPropTypes(validLoop, 'loop', getPropType('loop'), componentName)).toBe(true); + expect(checkPropTypes(invalidLoop, 'loop', getPropType('loop'), componentName)).toBe(false); + }); + + it('should validate the loop as a JSExpression', () => { + const validLoop = { type: 'JSExpression', value: 'items' }; + const invalidLoop = { type: 'JSExpression', value: 123 }; // Not a string + expect(checkPropTypes(validLoop, 'loop', getPropType('loop'), componentName)).toBe(true); + expect(checkPropTypes(invalidLoop, 'loop', getPropType('loop'), componentName)).toBe(false); + }); + + it('should validate the loopArgs as an array', () => { + const validLoopArgs = ['item']; + const invalidLoopArgs = 'Not an array'; // Not an array + expect(checkPropTypes(validLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); + expect(checkPropTypes(invalidLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(false); + }); + + it('should validate the loopArgs as a JSExpression', () => { + const validLoopArgs = { type: 'JSExpression', value: 'item' }; + const invalidLoopArgs = { type: 'JSExpression', value: 123 }; // Not a string + const validLoopArgs2 = [{ type: 'JSExpression', value: 'item' }, { type: 'JSExpression', value: 'index' }]; + expect(checkPropTypes(validLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); + expect(checkPropTypes(invalidLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(false); + expect(checkPropTypes(validLoopArgs2, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); + }); + + it('should validate the children as an array', () => { + const validChildren = [{ + id: 'child1', + componentName: 'Button', + }, { + id: 'child2', + componentName: 'Button', + }]; + const invalidChildren = 'Not an array'; // Not an array + const invalidChildren2 = [{}]; // Not an valid array + expect(checkPropTypes(invalidChildren, 'children', getPropType('children'), componentName)).toBe(false); + expect(checkPropTypes(validChildren, 'children', getPropType('children'), componentName)).toBe(true); + expect(checkPropTypes(invalidChildren2, 'children', getPropType('children'), componentName)).toBe(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); diff --git a/packages/plugin-command/build.json b/packages/plugin-command/build.json new file mode 100644 index 0000000000..d0aec10385 --- /dev/null +++ b/packages/plugin-command/build.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + "@alilc/build-plugin-lce", + "build-plugin-fusion", + ["build-plugin-moment-locales", { + "locales": ["zh-cn"] + }] + ] +} diff --git a/packages/plugin-command/build.test.json b/packages/plugin-command/build.test.json new file mode 100644 index 0000000000..9596d43e79 --- /dev/null +++ b/packages/plugin-command/build.test.json @@ -0,0 +1,19 @@ +{ + "plugins": [ + [ + "@alilc/build-plugin-lce", + { + "filename": "editor-preset-vision", + "library": "LowcodeEditor", + "libraryTarget": "umd", + "externals": { + "react": "var window.React", + "react-dom": "var window.ReactDOM", + "prop-types": "var window.PropTypes", + "rax": "var window.Rax" + } + } + ], + "@alilc/lowcode-test-mate/plugin/index.ts" + ] +} diff --git a/packages/plugin-command/jest.config.js b/packages/plugin-command/jest.config.js new file mode 100644 index 0000000000..822a526b7d --- /dev/null +++ b/packages/plugin-command/jest.config.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const { join } = require('path'); +const esModules = [].join('|'); +const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); + +const jestConfig = { + transformIgnorePatterns: [ + `/node_modules/(?!${esModules})/`, + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.ts', + 'src/**/*.tsx', + ], +}; + +// 只对本仓库内的 pkg 做 mapping +jestConfig.moduleNameMapper = {}; +jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/plugin-command/package.json b/packages/plugin-command/package.json new file mode 100644 index 0000000000..4f53e69e36 --- /dev/null +++ b/packages/plugin-command/package.json @@ -0,0 +1,39 @@ +{ + "name": "@alilc/lowcode-plugin-command", + "version": "1.3.2", + "description": "> TODO: description", + "author": "liujuping ", + "homepage": "https://github.com/alibaba/lowcode-engine#readme", + "license": "ISC", + "main": "lib/index.js", + "module": "es/index.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib", + "es" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alibaba/lowcode-engine.git" + }, + "scripts": { + "test": "build-scripts test --config build.test.json --jest-passWithNoTests", + "build": "build-scripts build" + }, + "bugs": { + "url": "https://github.com/alibaba/lowcode-engine/issues" + }, + "dependencies": { + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.18" + } +} diff --git a/packages/plugin-command/src/history-command.ts b/packages/plugin-command/src/history-command.ts new file mode 100644 index 0000000000..ea7e491bce --- /dev/null +++ b/packages/plugin-command/src/history-command.ts @@ -0,0 +1,43 @@ +import { IPublicModelPluginContext, IPublicTypePlugin } from '@alilc/lowcode-types'; + +export const historyCommand: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { + const { command, project } = ctx; + return { + init() { + command.registerCommand({ + name: 'undo', + description: 'Undo the last operation.', + handler: () => { + const state = project.currentDocument?.history.getState() || 0; + const enable = !!(state & 1); + if (!enable) { + throw new Error('Can not undo.'); + } + project.currentDocument?.history.back(); + }, + }); + + command.registerCommand({ + name: 'redo', + description: 'Redo the last operation.', + handler: () => { + const state = project.currentDocument?.history.getState() || 0; + const enable = !!(state & 2); + if (!enable) { + throw new Error('Can not redo.'); + } + project.currentDocument?.history.forward(); + }, + }); + }, + destroy() { + command.unregisterCommand('history:undo'); + command.unregisterCommand('history:redo'); + }, + }; +}; + +historyCommand.pluginName = '___history_command___'; +historyCommand.meta = { + commandScope: 'history', +}; diff --git a/packages/plugin-command/src/index.ts b/packages/plugin-command/src/index.ts new file mode 100644 index 0000000000..fa6f32b32d --- /dev/null +++ b/packages/plugin-command/src/index.ts @@ -0,0 +1,25 @@ +import { IPublicModelPluginContext, IPublicTypePlugin } from '@alilc/lowcode-types'; +import { nodeCommand } from './node-command'; +import { historyCommand } from './history-command'; + +export const CommandPlugin: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { + const { plugins } = ctx; + + return { + async init() { + await plugins.register(nodeCommand, {}, { autoInit: true }); + await plugins.register(historyCommand, {}, { autoInit: true }); + }, + destroy() { + plugins.delete(nodeCommand.pluginName); + plugins.delete(historyCommand.pluginName); + }, + }; +}; + +CommandPlugin.pluginName = '___default_command___'; +CommandPlugin.meta = { + commandScope: 'common', +}; + +export default CommandPlugin; \ No newline at end of file diff --git a/packages/plugin-command/src/node-command.ts b/packages/plugin-command/src/node-command.ts new file mode 100644 index 0000000000..eeda1d1688 --- /dev/null +++ b/packages/plugin-command/src/node-command.ts @@ -0,0 +1,497 @@ +import { IPublicModelPluginContext, IPublicTypeNodeSchema, IPublicTypePlugin, IPublicTypePropType } from '@alilc/lowcode-types'; +import { isNodeSchema } from '@alilc/lowcode-utils'; + +const sampleNodeSchema: IPublicTypePropType = { + type: 'shape', + value: [ + { + name: 'id', + propType: 'string', + }, + { + name: 'componentName', + propType: { + type: 'string', + isRequired: true, + }, + }, + { + name: 'props', + propType: 'object', + }, + { + name: 'condition', + propType: 'any', + }, + { + name: 'loop', + propType: 'any', + }, + { + name: 'loopArgs', + propType: 'any', + }, + { + name: 'children', + propType: 'any', + }, + ], +}; + +export const nodeSchemaPropType: IPublicTypePropType = { + type: 'shape', + value: [ + sampleNodeSchema.value[0], + sampleNodeSchema.value[1], + { + name: 'props', + propType: { + type: 'objectOf', + value: { + type: 'oneOfType', + // 不会强制校验,更多作为提示 + value: [ + 'any', + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSFunction'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSSlot'], + }, + }, + { + name: 'value', + propType: { + type: 'oneOfType', + value: [ + sampleNodeSchema, + { + type: 'arrayOf', + value: sampleNodeSchema, + }, + ], + }, + }, + ], + }, + ], + }, + }, + }, + { + name: 'condition', + propType: { + type: 'oneOfType', + value: [ + 'bool', + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + ], + }, + }, + { + name: 'loop', + propType: { + type: 'oneOfType', + value: [ + 'array', + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + ], + }, + }, + { + name: 'loopArgs', + propType: { + type: 'oneOfType', + value: [ + { + type: 'arrayOf', + value: { + type: 'oneOfType', + value: [ + 'any', + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + ], + }, + }, + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + }, + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + ], + }, + }, + { + name: 'children', + propType: { + type: 'arrayOf', + value: sampleNodeSchema, + }, + }, + ], +}; + +export const nodeCommand: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { + const { command, project } = ctx; + return { + init() { + command.registerCommand({ + name: 'add', + description: 'Add a node to the canvas.', + handler: (param: { + parentNodeId: string; + nodeSchema: IPublicTypeNodeSchema; + index: number; + }) => { + const { + parentNodeId, + nodeSchema, + index, + } = param; + const { project } = ctx; + const parentNode = project.currentDocument?.getNodeById(parentNodeId); + if (!parentNode) { + throw new Error(`Can not find node '${parentNodeId}'.`); + } + + if (!parentNode.isContainerNode) { + throw new Error(`Node '${parentNodeId}' is not a container node.`); + } + + if (!isNodeSchema(nodeSchema)) { + throw new Error('Invalid node.'); + } + + if (index < 0 || index > (parentNode.children?.size || 0)) { + throw new Error(`Invalid index '${index}'.`); + } + + project.currentDocument?.insertNode(parentNode, nodeSchema, index); + }, + parameters: [ + { + name: 'parentNodeId', + propType: 'string', + description: 'The id of the parent node.', + }, + { + name: 'nodeSchema', + propType: nodeSchemaPropType, + description: 'The node to be added.', + }, + { + name: 'index', + propType: 'number', + description: 'The index of the node to be added.', + }, + ], + }); + + command.registerCommand({ + name: 'move', + description: 'Move a node to another node.', + handler(param: { + nodeId: string; + targetNodeId: string; + index: number; + }) { + const { + nodeId, + targetNodeId, + 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) { + throw new Error(`Can not find node '${nodeId}'.`); + } + + if (!targetNode) { + throw new Error(`Can not find node '${targetNodeId}'.`); + } + + if (!targetNode.isContainerNode) { + throw new Error(`Node '${targetNodeId}' is not a container node.`); + } + + if (index < 0 || index > (targetNode.children?.size || 0)) { + throw new Error(`Invalid index '${index}'.`); + } + + project.currentDocument?.removeNode(node); + project.currentDocument?.insertNode(targetNode, node, index); + }, + parameters: [ + { + name: 'nodeId', + propType: { + type: 'string', + isRequired: true, + }, + description: 'The id of the node to be moved.', + }, + { + name: 'targetNodeId', + propType: { + type: 'string', + isRequired: true, + }, + description: 'The id of the target node.', + }, + { + name: 'index', + propType: 'number', + description: 'The index of the node to be moved.', + }, + ], + }); + + command.registerCommand({ + name: 'remove', + description: 'Remove a node from the canvas.', + handler(param: { + nodeId: string; + }) { + const { + nodeId, + } = param; + + const node = project.currentDocument?.getNodeById(nodeId); + if (!node) { + throw new Error(`Can not find node '${nodeId}'.`); + } + + project.currentDocument?.removeNode(node); + }, + parameters: [ + { + name: 'nodeId', + propType: 'string', + description: 'The id of the node to be removed.', + }, + ], + }); + + command.registerCommand({ + name: 'update', + description: 'Update a node.', + handler(param: { + nodeId: string; + nodeSchema: IPublicTypeNodeSchema; + }) { + const { + nodeId, + nodeSchema, + } = param; + + const node = project.currentDocument?.getNodeById(nodeId); + if (!node) { + throw new Error(`Can not find node '${nodeId}'.`); + } + + if (!isNodeSchema(nodeSchema)) { + throw new Error('Invalid node.'); + } + + node.importSchema(nodeSchema); + }, + parameters: [ + { + name: 'nodeId', + propType: 'string', + description: 'The id of the node to be updated.', + }, + { + name: 'nodeSchema', + propType: nodeSchemaPropType, + description: 'The node to be updated.', + }, + ], + }); + + command.registerCommand({ + name: 'updateProps', + description: 'Update the properties of a node.', + handler(param: { + nodeId: string; + props: Record; + }) { + const { + nodeId, + props, + } = param; + + const node = project.currentDocument?.getNodeById(nodeId); + if (!node) { + throw new Error(`Can not find node '${nodeId}'.`); + } + + Object.keys(props).forEach(key => { + node.setPropValue(key, props[key]); + }); + }, + parameters: [ + { + name: 'nodeId', + propType: 'string', + description: 'The id of the node to be updated.', + }, + { + name: 'props', + propType: 'object', + description: 'The properties to be updated.', + }, + ], + }); + + command.registerCommand({ + name: 'removeProps', + description: 'Remove the properties of a node.', + handler(param: { + nodeId: string; + propNames: string[]; + }) { + const { + nodeId, + propNames, + } = param; + + const node = project.currentDocument?.getNodeById(nodeId); + if (!node) { + throw new Error(`Can not find node '${nodeId}'.`); + } + + propNames.forEach(key => { + node.props?.getProp(key)?.remove(); + }); + }, + parameters: [ + { + name: 'nodeId', + propType: 'string', + description: 'The id of the node to be updated.', + }, + { + name: 'propNames', + propType: 'array', + description: 'The properties to be removed.', + }, + ], + }); + }, + destroy() { + command.unregisterCommand('node:add'); + command.unregisterCommand('node:move'); + command.unregisterCommand('node:remove'); + command.unregisterCommand('node:update'); + command.unregisterCommand('node:updateProps'); + command.unregisterCommand('node:removeProps'); + }, + }; +}; + +nodeCommand.pluginName = '___node_command___'; +nodeCommand.meta = { + commandScope: 'node', +}; + diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index 9fb9a75655..ac25097c78 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-designer", - "version": "1.3.1", + "version": "1.3.2", "description": "alibaba lowcode editor designer plugin", "files": [ "es", @@ -18,9 +18,9 @@ ], "author": "xiayang.xy", "dependencies": { - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "react": "^16.8.1", "react-dom": "^16.8.1" }, diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json index 1539ac6ae3..f50f71cbfe 100644 --- a/packages/plugin-outline-pane/package.json +++ b/packages/plugin-outline-pane/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-outline-pane", - "version": "1.3.1", + "version": "1.3.2", "description": "Outline pane for Ali lowCode engine", "files": [ "es", @@ -13,8 +13,8 @@ }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "react": "^16", "react-dom": "^16.7.0", diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index b041823e85..625801bc25 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-react-renderer", - "version": "1.3.1", + "version": "1.3.2", "description": "react renderer for ali lowcode engine", "main": "lib/index.js", "module": "es/index.js", @@ -22,7 +22,7 @@ ], "dependencies": { "@alifd/next": "^1.21.16", - "@alilc/lowcode-renderer-core": "1.3.1" + "@alilc/lowcode-renderer-core": "1.3.2" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", diff --git a/packages/react-simulator-renderer/package.json b/packages/react-simulator-renderer/package.json index 1361535099..3c3950a124 100644 --- a/packages/react-simulator-renderer/package.json +++ b/packages/react-simulator-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-react-simulator-renderer", - "version": "1.3.1", + "version": "1.3.2", "description": "react simulator renderer for alibaba lowcode designer", "main": "lib/index.js", "module": "es/index.js", @@ -17,10 +17,10 @@ "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "dependencies": { - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-react-renderer": "1.3.1", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-react-renderer": "1.3.2", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "mobx": "^6.3.0", "mobx-react": "^7.2.0", diff --git a/packages/react-simulator-renderer/src/renderer.ts b/packages/react-simulator-renderer/src/renderer.ts index efebeda040..20f6e18c0b 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/renderer-core/package.json b/packages/renderer-core/package.json index dd48d361f1..199eac1cac 100644 --- a/packages/renderer-core/package.json +++ b/packages/renderer-core/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-renderer-core", - "version": "1.3.1", + "version": "1.3.2", "description": "renderer core", "license": "MIT", "main": "lib/index.js", @@ -16,8 +16,8 @@ }, "dependencies": { "@alilc/lowcode-datasource-engine": "^1.0.0", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "debug": "^4.1.1", "fetch-jsonp": "^1.1.3", @@ -32,7 +32,7 @@ "devDependencies": { "@alib/build-scripts": "^0.1.18", "@alifd/next": "^1.26.0", - "@alilc/lowcode-designer": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", "@babel/plugin-transform-typescript": "^7.16.8", "@testing-library/react": "^11.2.2", "@types/classnames": "^2.2.11", diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 9d9e88ab59..d240095604 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -4,7 +4,7 @@ import classnames from 'classnames'; import { create as createDataSourceEngine } from '@alilc/lowcode-datasource-engine/interpret'; import { IPublicTypeNodeSchema, IPublicTypeNodeData, IPublicTypeJSONValue, IPublicTypeCompositeValue } from '@alilc/lowcode-types'; -import { isI18nData, isJSExpression, isJSFunction } from '@alilc/lowcode-utils'; +import { checkPropTypes, isI18nData, isJSExpression, isJSFunction } from '@alilc/lowcode-utils'; import adapter from '../adapter'; import divFactory from '../components/Div'; import visualDomFactory from '../components/VisualDom'; @@ -21,7 +21,6 @@ import { isFileSchema, transformArrayToMap, transformStringToFunction, - checkPropTypes, getI18n, getFileCssName, capitalizeFirstLetter, diff --git a/packages/renderer-core/src/utils/common.ts b/packages/renderer-core/src/utils/common.ts index 495744acd9..0462d358a7 100644 --- a/packages/renderer-core/src/utils/common.ts +++ b/packages/renderer-core/src/utils/common.ts @@ -6,16 +6,11 @@ import { isI18nData, isJSExpression } from '@alilc/lowcode-utils'; import { isEmpty } from 'lodash'; import IntlMessageFormat from 'intl-messageformat'; import pkg from '../../package.json'; -import * as ReactIs from 'react-is'; -import { default as ReactPropTypesSecret } from 'prop-types/lib/ReactPropTypesSecret'; -import { default as factoryWithTypeCheckers } from 'prop-types/factoryWithTypeCheckers'; (window as any).sdkVersion = pkg.version; export { pick, isEqualWith as deepEqual, cloneDeep as clone, isEmpty, throttle, debounce } from 'lodash'; -const PropTypes2 = factoryWithTypeCheckers(ReactIs.isElement, true); - const EXPRESSION_TYPE = { JSEXPRESSION: 'JSExpression', JSFUNCTION: 'JSFunction', @@ -183,31 +178,6 @@ export function transformArrayToMap(arr: any[], key: string, overwrite = true) { return res; } -export function checkPropTypes(value: any, name: string, rule: any, componentName: string): boolean { - let ruleFunction = rule; - if (typeof rule === 'string') { - ruleFunction = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2); - } - if (!ruleFunction || typeof ruleFunction !== 'function') { - logger.warn('checkPropTypes should have a function type rule argument'); - return true; - } - const err = ruleFunction( - { - [name]: value, - }, - name, - componentName, - 'prop', - null, - ReactPropTypesSecret, - ); - if (err) { - logger.warn(err); - } - return !err; -} - /** * transform string to a function * @param str function in string form diff --git a/packages/renderer-core/tests/utils/common.test.ts b/packages/renderer-core/tests/utils/common.test.ts index 2ee3ed4dcc..13b6908d50 100644 --- a/packages/renderer-core/tests/utils/common.test.ts +++ b/packages/renderer-core/tests/utils/common.test.ts @@ -1,4 +1,3 @@ -import factoryWithTypeCheckers from 'prop-types/factoryWithTypeCheckers'; import { isSchema, isFileSchema, @@ -18,14 +17,9 @@ import { parseThisRequiredExpression, parseI18n, parseData, - checkPropTypes, } from '../../src/utils/common'; import logger from '../../src/utils/logger'; -var ReactIs = require('react-is'); - -const PropTypes = factoryWithTypeCheckers(ReactIs.isElement, true); - describe('test isSchema', () => { it('should be false when empty value is passed', () => { expect(isSchema(null)).toBeFalsy(); @@ -283,7 +277,7 @@ describe('test capitalizeFirstLetter ', () => { describe('test forEach ', () => { it('should work', () => { const mockFn = jest.fn(); - + forEach(null, mockFn); expect(mockFn).toBeCalledTimes(0); @@ -298,7 +292,7 @@ describe('test forEach ', () => { forEach({ a: 1, b: 2, c: 3 }, mockFn); expect(mockFn).toBeCalledTimes(3); - + const mockFn2 = jest.fn(); forEach({ a: 1 }, mockFn2, { b: 'bbb' }); expect(mockFn2).toHaveBeenCalledWith(1, 'a'); @@ -467,36 +461,3 @@ describe('test parseData ', () => { }); }); - -describe('checkPropTypes', () => { - it('should validate correctly with valid prop type', () => { - expect(checkPropTypes(123, 'age', PropTypes.number, 'TestComponent')).toBe(true); - expect(checkPropTypes('123', 'age', PropTypes.string, 'TestComponent')).toBe(true); - }); - - it('should log a warning and return false with invalid prop type', () => { - expect(checkPropTypes(123, 'age', PropTypes.string, 'TestComponent')).toBe(false); - expect(checkPropTypes('123', 'age', PropTypes.number, 'TestComponent')).toBe(false); - }); - - it('should handle custom rule functions correctly', () => { - const customRule = (props, propName) => { - if (props[propName] !== 123) { - return new Error('Invalid value'); - } - }; - const result = checkPropTypes(123, 'customProp', customRule, 'TestComponent'); - expect(result).toBe(true); - }); - - - it('should interpret and validate a rule given as a string', () => { - const result = checkPropTypes(123, 'age', 'PropTypes.number', 'TestComponent'); - expect(result).toBe(true); - }); - - it('should log a warning for invalid rule type', () => { - const result = checkPropTypes(123, 'age', 123, 'TestComponent'); - expect(result).toBe(true); - }); -}); \ No newline at end of file diff --git a/packages/shell/package.json b/packages/shell/package.json index 87f71a5ee6..c2b62e2270 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-shell", - "version": "1.3.1", + "version": "1.3.2", "description": "Shell Layer for AliLowCodeEngine", "main": "lib/index.js", "module": "es/index.js", @@ -13,12 +13,12 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-editor-skeleton": "1.3.1", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", - "@alilc/lowcode-workspace": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-editor-skeleton": "1.3.2", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", + "@alilc/lowcode-workspace": "1.3.2", "classnames": "^2.2.6", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", diff --git a/packages/shell/src/api/command.ts b/packages/shell/src/api/command.ts new file mode 100644 index 0000000000..ebab4a9ff5 --- /dev/null +++ b/packages/shell/src/api/command.ts @@ -0,0 +1,46 @@ +import { IPublicApiCommand, IPublicModelPluginContext, IPublicTypeCommand, IPublicTypeCommandHandlerArgs, IPublicTypeListCommand } from '@alilc/lowcode-types'; +import { commandSymbol, pluginContextSymbol } from '../symbols'; +import { ICommand, ICommandOptions } from '@alilc/lowcode-editor-core'; + +const optionsSymbol = Symbol('options'); +const commandScopeSet = new Set(); + +export class Command implements IPublicApiCommand { + [commandSymbol]: ICommand; + [optionsSymbol]?: ICommandOptions; + [pluginContextSymbol]?: IPublicModelPluginContext; + + constructor(innerCommand: ICommand, pluginContext?: IPublicModelPluginContext, options?: ICommandOptions) { + this[commandSymbol] = innerCommand; + this[optionsSymbol] = options; + this[pluginContextSymbol] = pluginContext; + const commandScope = options?.commandScope; + if (commandScope && commandScopeSet.has(commandScope)) { + throw new Error(`Command scope "${commandScope}" has been registered.`); + } + } + + registerCommand(command: IPublicTypeCommand): void { + this[commandSymbol].registerCommand(command, this[optionsSymbol]); + } + + batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[]): void { + this[commandSymbol].batchExecuteCommand(commands, this[pluginContextSymbol]); + } + + executeCommand(name: string, args: IPublicTypeCommandHandlerArgs): void { + this[commandSymbol].executeCommand(name, args); + } + + listCommands(): IPublicTypeListCommand[] { + return this[commandSymbol].listCommands(); + } + + unregisterCommand(name: string): void { + this[commandSymbol].unregisterCommand(name); + } + + onCommandError(callback: (name: string, error: Error) => void): void { + this[commandSymbol].onCommandError(callback); + } +} diff --git a/packages/shell/src/api/index.ts b/packages/shell/src/api/index.ts index 3726020de0..79340f6777 100644 --- a/packages/shell/src/api/index.ts +++ b/packages/shell/src/api/index.ts @@ -11,4 +11,5 @@ export * from './skeleton'; export * from './canvas'; export * from './workspace'; export * from './config'; -export * from './commonUI'; \ No newline at end of file +export * from './commonUI'; +export * from './command'; \ No newline at end of file diff --git a/packages/shell/src/api/material.ts b/packages/shell/src/api/material.ts index f0c37d8a4d..284b88fbbf 100644 --- a/packages/shell/src/api/material.ts +++ b/packages/shell/src/api/material.ts @@ -3,7 +3,7 @@ import { IDesigner, isComponentMeta, } from '@alilc/lowcode-designer'; -import { IPublicTypeAssetsJson } from '@alilc/lowcode-utils'; +import { IPublicTypeAssetsJson, getLogger } from '@alilc/lowcode-utils'; import { IPublicTypeComponentAction, IPublicTypeComponentMetadata, @@ -21,6 +21,8 @@ import { editorSymbol, designerSymbol } from '../symbols'; import { ComponentMeta as ShellComponentMeta } from '../model'; import { ComponentType } from 'react'; +const logger = getLogger({ level: 'warn', bizName: 'shell-material' }); + const innerEditorSymbol = Symbol('editor'); export class Material implements IPublicApiMaterial { private readonly [innerEditorSymbol]: IPublicModelEditor; @@ -31,6 +33,10 @@ export class Material implements IPublicApiMaterial { } const workspace: InnerWorkspace = globalContext.get('workspace'); if (workspace.isActive) { + if (!workspace.window.editor) { + logger.error('Material api 调用时机出现问题,请检查'); + return this[innerEditorSymbol]; + } return workspace.window.editor; } diff --git a/packages/shell/src/api/workspace.ts b/packages/shell/src/api/workspace.ts index fd3e82fa90..f5bc79009f 100644 --- a/packages/shell/src/api/workspace.ts +++ b/packages/shell/src/api/workspace.ts @@ -90,7 +90,7 @@ export class Workspace implements IPublicApiWorkspace { } get plugins() { - return new Plugins(this[workspaceSymbol].plugins, true); + return new Plugins(this[workspaceSymbol].plugins, true).toProxy(); } get skeleton() { diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx index ccd910efc4..8c7ab446ba 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/shell/src/index.ts b/packages/shell/src/index.ts index ce09ccaaa7..fb1e7228f3 100644 --- a/packages/shell/src/index.ts +++ b/packages/shell/src/index.ts @@ -29,6 +29,7 @@ import { SimulatorHost, Config, CommonUI, + Command, } from './api'; export * from './symbols'; @@ -70,4 +71,5 @@ export { SettingField, SkeletonItem, CommonUI, + Command, }; diff --git a/packages/shell/src/model/window.ts b/packages/shell/src/model/window.ts index 2b5e0dd8c3..1bc84e661c 100644 --- a/packages/shell/src/model/window.ts +++ b/packages/shell/src/model/window.ts @@ -48,8 +48,8 @@ export class Window implements IPublicModelWindow { } get currentEditorView() { - if (this[windowSymbol].editorView) { - return new EditorView(this[windowSymbol].editorView).toProxy() as any; + if (this[windowSymbol]._editorView) { + return new EditorView(this[windowSymbol]._editorView).toProxy() as any; } return null; } diff --git a/packages/shell/src/symbols.ts b/packages/shell/src/symbols.ts index 8e2962a24f..e0f846ad36 100644 --- a/packages/shell/src/symbols.ts +++ b/packages/shell/src/symbols.ts @@ -39,4 +39,5 @@ export const configSymbol = Symbol('configSymbol'); export const conditionGroupSymbol = Symbol('conditionGroup'); export const editorViewSymbol = Symbol('editorView'); export const pluginContextSymbol = Symbol('pluginContext'); -export const skeletonItemSymbol = Symbol('skeletonItem'); \ No newline at end of file +export const skeletonItemSymbol = Symbol('skeletonItem'); +export const commandSymbol = Symbol('command'); \ No newline at end of file diff --git a/packages/types/package.json b/packages/types/package.json index 31dadb5ea0..5651d427d4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-types", - "version": "1.3.1", + "version": "1.3.2", "description": "Types for Ali lowCode engine", "files": [ "es", diff --git a/packages/types/src/shell/api/command.ts b/packages/types/src/shell/api/command.ts new file mode 100644 index 0000000000..1f8425dcef --- /dev/null +++ b/packages/types/src/shell/api/command.ts @@ -0,0 +1,34 @@ +import { IPublicTypeCommand, IPublicTypeCommandHandlerArgs, IPublicTypeListCommand } from '../type'; + +export interface IPublicApiCommand { + + /** + * 注册一个新命令及其处理函数 + */ + registerCommand(command: IPublicTypeCommand): void; + + /** + * 注销一个已存在的命令 + */ + unregisterCommand(name: string): void; + + /** + * 通过名称和给定参数执行一个命令,会校验参数是否符合命令定义 + */ + executeCommand(name: string, args?: IPublicTypeCommandHandlerArgs): void; + + /** + * 批量执行命令,执行完所有命令后再进行一次重绘,历史记录中只会记录一次 + */ + batchExecuteCommand(commands: { name: string; args?: IPublicTypeCommandHandlerArgs }[]): void; + + /** + * 列出所有已注册的命令 + */ + listCommands(): IPublicTypeListCommand[]; + + /** + * 注册错误处理回调函数 + */ + onCommandError(callback: (name: string, error: Error) => void): void; +} \ No newline at end of file diff --git a/packages/types/src/shell/api/index.ts b/packages/types/src/shell/api/index.ts index 79f1b0dc7c..8f14d8dadd 100644 --- a/packages/types/src/shell/api/index.ts +++ b/packages/types/src/shell/api/index.ts @@ -11,3 +11,4 @@ export * from './logger'; export * from './canvas'; export * from './workspace'; export * from './commonUI'; +export * from './command'; \ No newline at end of file diff --git a/packages/types/src/shell/api/workspace.ts b/packages/types/src/shell/api/workspace.ts index 9e1080b31e..b6e7d84cb7 100644 --- a/packages/types/src/shell/api/workspace.ts +++ b/packages/types/src/shell/api/workspace.ts @@ -1,8 +1,9 @@ import { IPublicModelWindow } from '../model'; -import { IPublicApiPlugins, IPublicModelResource, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; +import { IPublicApiPlugins, IPublicApiSkeleton, IPublicModelResource, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; export interface IPublicApiWorkspace< Plugins = IPublicApiPlugins, + Skeleton = IPublicApiSkeleton, ModelWindow = IPublicModelWindow, Resource = IPublicModelResource, > { @@ -15,6 +16,8 @@ export interface IPublicApiWorkspace< plugins: Plugins; + skeleton: Skeleton; + /** 当前设计器的编辑窗口 */ windows: ModelWindow[]; diff --git a/packages/types/src/shell/model/plugin-context.ts b/packages/types/src/shell/model/plugin-context.ts index 45568424de..d4d715e96b 100644 --- a/packages/types/src/shell/model/plugin-context.ts +++ b/packages/types/src/shell/model/plugin-context.ts @@ -12,6 +12,7 @@ import { IPublicApiPlugins, IPublicApiWorkspace, IPublicApiCommonUI, + IPublicApiCommand, } from '../api'; import { IPublicEnumPluginRegisterLevel } from '../enum'; import { IPublicModelEngineConfig, IPublicModelWindow } from './'; @@ -109,6 +110,8 @@ export interface IPublicModelPluginContext { */ get commonUI(): IPublicApiCommonUI; + get command(): IPublicApiCommand; + /** * 插件注册层级 * @since v1.1.7 diff --git a/packages/types/src/shell/type/command.ts b/packages/types/src/shell/type/command.ts new file mode 100644 index 0000000000..0f301bd658 --- /dev/null +++ b/packages/types/src/shell/type/command.ts @@ -0,0 +1,59 @@ +import { IPublicTypePropType } from './prop-types'; + +// 定义命令处理函数的参数类型 +export interface IPublicTypeCommandHandlerArgs { + [key: string]: any; +} + +// 定义命令参数的接口 +export interface IPublicTypeCommandParameter { + + /** + * 参数名称 + */ + name: string; + + /** + * 参数类型或详细类型描述 + */ + propType: string | IPublicTypePropType; + + /** + * 参数描述 + */ + description: string; + + /** + * 参数默认值(可选) + */ + defaultValue?: any; +} + +// 定义单个命令的接口 +export interface IPublicTypeCommand { + + /** + * 命令名称 + * 命名规则:commandName + * 使用规则:commandScope:commandName (commandScope 在插件 meta 中定义,用于区分不同插件的命令) + */ + name: string; + + /** + * 命令参数 + */ + parameters?: IPublicTypeCommandParameter[]; + + /** + * 命令描述 + */ + description?: string; + + /** + * 命令处理函数 + */ + handler: (args: any) => void; +} + +export interface IPublicTypeListCommand extends Pick { +} \ No newline at end of file diff --git a/packages/types/src/shell/type/field-extra-props.ts b/packages/types/src/shell/type/field-extra-props.ts index 3e2df280b7..7aae7e0fe8 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/index.ts b/packages/types/src/shell/type/index.ts index b1c7779d05..76dd389255 100644 --- a/packages/types/src/shell/type/index.ts +++ b/packages/types/src/shell/type/index.ts @@ -92,4 +92,5 @@ export * from './hotkey-callbacks'; export * from './scrollable'; export * from './simulator-renderer'; export * from './config-transducer'; -export * from './context-menu'; \ No newline at end of file +export * from './context-menu'; +export * from './command'; \ No newline at end of file diff --git a/packages/types/src/shell/type/plugin-meta.ts b/packages/types/src/shell/type/plugin-meta.ts index 421e59ad0a..bf7f6212e8 100644 --- a/packages/types/src/shell/type/plugin-meta.ts +++ b/packages/types/src/shell/type/plugin-meta.ts @@ -1,14 +1,17 @@ import { IPublicTypePluginDeclaration } from './'; export interface IPublicTypePluginMeta { + /** * define dependencies which the plugin depends on */ dependencies?: string[]; + /** * specify which engine version is compatible with the plugin */ engines?: { + /** e.g. '^1.0.0' */ lowcodeEngine?: string; }; @@ -26,4 +29,9 @@ export interface IPublicTypePluginMeta { * event.emit('someEventName') is actually sending event with name 'myEvent:someEventName' */ eventPrefix?: string; + + /** + * 如果要使用 command 注册命令,需要在插件 meta 中定义 commandScope + */ + commandScope?: string; } diff --git a/packages/types/src/shell/type/registered-setter.ts b/packages/types/src/shell/type/registered-setter.ts index 85cad5a803..55a90465a8 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; diff --git a/packages/utils/package.json b/packages/utils/package.json index 12e3ebc1b3..60605d81e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-utils", - "version": "1.3.1", + "version": "1.3.2", "description": "Utils for Ali lowCode engine", "files": [ "lib", @@ -14,9 +14,10 @@ }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.3.1", + "@alilc/lowcode-types": "1.3.2", "lodash": "^4.17.21", "mobx": "^6.3.0", + "prop-types": "^15.8.1", "react": "^16" }, "devDependencies": { diff --git a/packages/utils/src/check-prop-types.ts b/packages/utils/src/check-prop-types.ts new file mode 100644 index 0000000000..dc9ce31ed5 --- /dev/null +++ b/packages/utils/src/check-prop-types.ts @@ -0,0 +1,72 @@ +import * as ReactIs from 'react-is'; +import { default as ReactPropTypesSecret } from 'prop-types/lib/ReactPropTypesSecret'; +import { default as factoryWithTypeCheckers } from 'prop-types/factoryWithTypeCheckers'; +import { IPublicTypePropType } from '@alilc/lowcode-types'; +import { isRequiredPropType } from './check-types/is-required-prop-type'; +import { Logger } from './logger'; + +const PropTypes2 = factoryWithTypeCheckers(ReactIs.isElement, true); +const logger = new Logger({ level: 'warn', bizName: 'utils' }); + +export function transformPropTypesRuleToString(rule: IPublicTypePropType | string): string { + if (!rule) { + return 'PropTypes.any'; + } + + if (typeof rule === 'string') { + return rule.startsWith('PropTypes.') ? rule : `PropTypes.${rule}`; + } + + if (isRequiredPropType(rule)) { + const { type, isRequired } = rule; + return `PropTypes.${type}${isRequired ? '.isRequired' : ''}`; + } + + const { type, value } = rule; + switch (type) { + case 'oneOf': + return `PropTypes.oneOf([${value.map((item: any) => `"${item}"`).join(',')}])`; + case 'oneOfType': + return `PropTypes.oneOfType([${value.map((item: any) => transformPropTypesRuleToString(item)).join(', ')}])`; + case 'arrayOf': + case 'objectOf': + return `PropTypes.${type}(${transformPropTypesRuleToString(value)})`; + case 'shape': + case 'exact': + return `PropTypes.${type}({${value.map((item: any) => `${item.name}: ${transformPropTypesRuleToString(item.propType)}`).join(',')}})`; + default: + logger.error(`Unknown prop type: ${type}`); + } + + return 'PropTypes.any'; +} + +export function checkPropTypes(value: any, name: string, rule: any, componentName: string): boolean { + let ruleFunction = rule; + if (typeof rule === 'object') { + // eslint-disable-next-line no-new-func + ruleFunction = new Function(`"use strict"; const PropTypes = arguments[0]; return ${transformPropTypesRuleToString(rule)}`)(PropTypes2); + } + if (typeof rule === 'string') { + // eslint-disable-next-line no-new-func + ruleFunction = new Function(`"use strict"; const PropTypes = arguments[0]; return ${transformPropTypesRuleToString(rule)}`)(PropTypes2); + } + if (!ruleFunction || typeof ruleFunction !== 'function') { + logger.warn('checkPropTypes should have a function type rule argument'); + return true; + } + const err = ruleFunction( + { + [name]: value, + }, + name, + componentName, + 'prop', + null, + ReactPropTypesSecret, + ); + if (err) { + logger.warn(err); + } + return !err; +} diff --git a/packages/utils/src/check-types/index.ts b/packages/utils/src/check-types/index.ts index 3155926ef2..507259b2c5 100644 --- a/packages/utils/src/check-types/index.ts +++ b/packages/utils/src/check-types/index.ts @@ -23,4 +23,6 @@ export * from './is-location-data'; export * from './is-setting-field'; export * from './is-lowcode-component-type'; export * from './is-lowcode-project-schema'; -export * from './is-component-schema'; \ No newline at end of file +export * from './is-component-schema'; +export * from './is-basic-prop-type'; +export * from './is-required-prop-type'; \ No newline at end of file diff --git a/packages/utils/src/check-types/is-basic-prop-type.ts b/packages/utils/src/check-types/is-basic-prop-type.ts new file mode 100644 index 0000000000..fd3b1b1dcb --- /dev/null +++ b/packages/utils/src/check-types/is-basic-prop-type.ts @@ -0,0 +1,8 @@ +import { IPublicTypeBasicType, IPublicTypePropType } from '@alilc/lowcode-types'; + +export function isBasicPropType(propType: IPublicTypePropType): propType is IPublicTypeBasicType { + if (!propType) { + return false; + } + return typeof propType === 'string'; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-required-prop-type.ts b/packages/utils/src/check-types/is-required-prop-type.ts new file mode 100644 index 0000000000..106da78a00 --- /dev/null +++ b/packages/utils/src/check-types/is-required-prop-type.ts @@ -0,0 +1,8 @@ +import { IPublicTypePropType, IPublicTypeRequiredType } from '@alilc/lowcode-types'; + +export function isRequiredPropType(propType: IPublicTypePropType): propType is IPublicTypeRequiredType { + if (!propType) { + return false; + } + return typeof propType === 'object' && propType.type && ['array', 'bool', 'func', 'number', 'object', 'string', 'node', 'element', 'any'].includes(propType.type); +} \ No newline at end of file diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx index d783a96434..185abbb343 100644 --- a/packages/utils/src/context-menu.tsx +++ b/packages/utils/src/context-menu.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import React from 'react'; import './context-menu.scss'; -const logger = new Logger({ level: 'warn', bizName: 'designer' }); +const logger = new Logger({ level: 'warn', bizName: 'utils' }); const { Item, Divider, PopupItem } = Menu; const MAX_LEVEL = 2; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b99ab02073..22bad0e36e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -32,3 +32,4 @@ export { transactionManager } from './transaction-manager'; export * from './check-types'; export * from './workspace'; export * from './context-menu'; +export { checkPropTypes } from './check-prop-types'; \ No newline at end of file diff --git a/packages/utils/test/src/check-prop-types.test.ts b/packages/utils/test/src/check-prop-types.test.ts new file mode 100644 index 0000000000..74146f2d94 --- /dev/null +++ b/packages/utils/test/src/check-prop-types.test.ts @@ -0,0 +1,255 @@ +import { checkPropTypes, transformPropTypesRuleToString } from '../../src/check-prop-types'; +import PropTypes from 'prop-types'; + +describe('checkPropTypes', () => { + it('should validate correctly with valid prop type', () => { + expect(checkPropTypes(123, 'age', PropTypes.number, 'TestComponent')).toBe(true); + expect(checkPropTypes('123', 'age', PropTypes.string, 'TestComponent')).toBe(true); + }); + + it('should log a warning and return false with invalid prop type', () => { + expect(checkPropTypes(123, 'age', PropTypes.string, 'TestComponent')).toBe(false); + expect(checkPropTypes('123', 'age', PropTypes.number, 'TestComponent')).toBe(false); + }); + + it('should validate correctly with valid object prop type', () => { + expect(checkPropTypes({ a: 123 }, 'age', PropTypes.object, 'TestComponent')).toBe(true); + expect(checkPropTypes({ a: '123' }, 'age', PropTypes.object, 'TestComponent')).toBe(true); + }); + + it('should validate correctly with valid object string prop type', () => { + expect(checkPropTypes({ a: 123 }, 'age', 'object', 'TestComponent')).toBe(true); + expect(checkPropTypes({ a: '123' }, 'age', 'object', 'TestComponent')).toBe(true); + }); + + it('should validate correctly with valid isRequired prop type', () => { + const rule = { + type: 'string', + isRequired: true, + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.string.isRequired'); + expect(checkPropTypes('News', 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes(undefined, 'type', rule, 'TestComponent')).toBe(false); + }); + + it('should handle custom rule functions correctly', () => { + const customRule = (props, propName) => { + if (props[propName] !== 123) { + return new Error('Invalid value'); + } + }; + const result = checkPropTypes(123, 'customProp', customRule, 'TestComponent'); + expect(result).toBe(true); + }); + + + it('should interpret and validate a rule given as a string', () => { + const result = checkPropTypes(123, 'age', 'PropTypes.number', 'TestComponent'); + expect(result).toBe(true); + }); + + it('should interpret and validate a rule given as a string', () => { + expect(checkPropTypes(123, 'age', 'number', 'TestComponent')).toBe(true); + expect(checkPropTypes('123', 'age', 'string', 'TestComponent')).toBe(true); + }); + + it('should log a warning for invalid rule type', () => { + const result = checkPropTypes(123, 'age', 123, 'TestComponent'); + expect(result).toBe(true); + }); + + // oneOf + it('should validate correctly with valid oneOf prop type', () => { + const rule = { + type: 'oneOf', + value: ['News', 'Photos'], + } + expect(transformPropTypesRuleToString(rule)).toBe(`PropTypes.oneOf(["News","Photos"])`); + expect(checkPropTypes('News', 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes('Others', 'type', rule, 'TestComponent')).toBe(false); + }); + + // oneOfType + it('should validate correctly with valid oneOfType prop type', () => { + const rule = { + type: 'oneOfType', + value: ['string', 'number', { + type: 'array', + isRequired: true, + }], + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array.isRequired])'); + expect(checkPropTypes(['News', 'Photos'], 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes('News', 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes(123, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({}, 'type', rule, 'TestComponent')).toBe(false); + }); + + it('should validate correctly with valid oneOfType prop type', () => { + const rule = { + type: 'oneOfType', + value: [ + 'bool', + { + type: 'shape', + value: [ + { + name: 'type', + propType: { + type: 'oneOf', + value: ['JSExpression'], + } + }, + { + name: 'value', + propType: 'string', + }, + ], + }, + ], + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.oneOfType([PropTypes.bool, PropTypes.shape({type: PropTypes.oneOf(["JSExpression"]),value: PropTypes.string})])'); + expect(checkPropTypes(true, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ type: 'JSExpression', value: '1 + 1 === 2' }, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ type: 'JSExpression' }, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ type: 'JSExpression', value: 123 }, 'type', rule, 'TestComponent')).toBe(false); + }); + + it('should log a warning for invalid type', () => { + const rule = { + type: 'inval', + value: ['News', 'Photos'], + } + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.any'); + expect(checkPropTypes('News', 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes('Others', 'type', rule, 'TestComponent')).toBe(true); + }); + + // arrayOf + it('should validate correctly with valid arrayOf prop type', () => { + const rule = { + type: 'arrayOf', + value: { + type: 'string', + isRequired: true, + }, + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.arrayOf(PropTypes.string.isRequired)'); + expect(checkPropTypes(['News', 'Photos'], 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes(['News', 123], 'type', rule, 'TestComponent')).toBe(false); + }); + + // objectOf + it('should validate correctly with valid objectOf prop type', () => { + const rule = { + type: 'objectOf', + value: { + type: 'string', + isRequired: true, + }, + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.objectOf(PropTypes.string.isRequired)'); + expect(checkPropTypes({ a: 'News', b: 'Photos' }, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ a: 'News', b: 123 }, 'type', rule, 'TestComponent')).toBe(false); + }); + + // shape + it('should validate correctly with valid shape prop type', () => { + const rule = { + type: 'shape', + value: [ + { + name: 'a', + propType: { + type: 'string', + isRequired: true, + }, + }, + { + name: 'b', + propType: { + type: 'number', + isRequired: true, + }, + }, + ], + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.shape({a: PropTypes.string.isRequired,b: PropTypes.number.isRequired})'); + expect(checkPropTypes({ a: 'News', b: 123 }, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ a: 'News', b: 'Photos' }, 'type', rule, 'TestComponent')).toBe(false); + + // isRequired + const rule2 = { + type: 'shape', + value: [ + { + name: 'a', + propType: { + type: 'string', + isRequired: true, + }, + }, + { + name: 'b', + propType: { + type: 'number', + isRequired: false, + }, + }, + ], + }; + expect(transformPropTypesRuleToString(rule2)).toBe('PropTypes.shape({a: PropTypes.string.isRequired,b: PropTypes.number})'); + expect(checkPropTypes({ a: 'News', b: 123 }, 'type', rule2, 'TestComponent')).toBe(true); + expect(checkPropTypes({ b: 123 }, 'type', rule2, 'TestComponent')).toBe(false); + }); + + // exact + it('should validate correctly with valid exact prop type', () => { + const rule = { + type: 'exact', + value: [ + { + name: 'a', + propType: { + type: 'string', + isRequired: true, + }, + }, + { + name: 'b', + propType: { + type: 'number', + isRequired: true, + }, + }, + ], + }; + expect(transformPropTypesRuleToString(rule)).toBe('PropTypes.exact({a: PropTypes.string.isRequired,b: PropTypes.number.isRequired})'); + expect(checkPropTypes({ a: 'News', b: 123 }, 'type', rule, 'TestComponent')).toBe(true); + expect(checkPropTypes({ a: 'News', b: 'Photos' }, 'type', rule, 'TestComponent')).toBe(false); + + // isRequired + const rule2 = { + type: 'exact', + value: [ + { + name: 'a', + propType: { + type: 'string', + isRequired: true, + }, + }, + { + name: 'b', + propType: { + type: 'number', + isRequired: false, + }, + }, + ], + }; + expect(transformPropTypesRuleToString(rule2)).toBe('PropTypes.exact({a: PropTypes.string.isRequired,b: PropTypes.number})'); + expect(checkPropTypes({ a: 'News', b: 123 }, 'type', rule2, 'TestComponent')).toBe(true); + expect(checkPropTypes({ b: 123 }, 'type', rule2, 'TestComponent')).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/utils/test/src/check-types/is-basic-prop-type.test.ts b/packages/utils/test/src/check-types/is-basic-prop-type.test.ts new file mode 100644 index 0000000000..81a1bf0d34 --- /dev/null +++ b/packages/utils/test/src/check-types/is-basic-prop-type.test.ts @@ -0,0 +1,11 @@ +import { isBasicPropType } from '../../../src'; + +describe('test isBasicPropType ', () => { + it('should work', () => { + expect(isBasicPropType(null)).toBeFalsy(); + expect(isBasicPropType(undefined)).toBeFalsy(); + expect(isBasicPropType({})).toBeFalsy(); + expect(isBasicPropType({ type: 'any other type' })).toBeFalsy(); + expect(isBasicPropType('string')).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/packages/utils/test/src/check-types/is-required-prop-type.test.ts b/packages/utils/test/src/check-types/is-required-prop-type.test.ts new file mode 100644 index 0000000000..25515f9aab --- /dev/null +++ b/packages/utils/test/src/check-types/is-required-prop-type.test.ts @@ -0,0 +1,13 @@ +import { isRequiredPropType } from '../../../src'; + +describe('test isRequiredType', () => { + it('should work', () => { + expect(isRequiredPropType(null)).toBeFalsy(); + expect(isRequiredPropType(undefined)).toBeFalsy(); + expect(isRequiredPropType({})).toBeFalsy(); + expect(isRequiredPropType({ type: 'any other type' })).toBeFalsy(); + expect(isRequiredPropType('string')).toBeFalsy(); + expect(isRequiredPropType({ type: 'string' })).toBeTruthy(); + expect(isRequiredPropType({ type: 'string', isRequired: true })).toBeTruthy(); + }); +}) diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 7af4a7159c..778b8167f8 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-workspace", - "version": "1.3.1", + "version": "1.3.2", "description": "Shell Layer for AliLowCodeEngine", "main": "lib/index.js", "module": "es/index.js", @@ -15,11 +15,11 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-designer": "1.3.1", - "@alilc/lowcode-editor-core": "1.3.1", - "@alilc/lowcode-editor-skeleton": "1.3.1", - "@alilc/lowcode-types": "1.3.1", - "@alilc/lowcode-utils": "1.3.1", + "@alilc/lowcode-designer": "1.3.2", + "@alilc/lowcode-editor-core": "1.3.2", + "@alilc/lowcode-editor-skeleton": "1.3.2", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", diff --git a/packages/workspace/src/context/base-context.ts b/packages/workspace/src/context/base-context.ts index 2f6154788f..445677a618 100644 --- a/packages/workspace/src/context/base-context.ts +++ b/packages/workspace/src/context/base-context.ts @@ -5,6 +5,7 @@ import { commonEvent, IEngineConfig, IHotKey, + Command as InnerCommand, } from '@alilc/lowcode-editor-core'; import { Designer, @@ -33,6 +34,7 @@ import { Window, Canvas, CommonUI, + Command, } from '@alilc/lowcode-shell'; import { IPluginPreferenceMananger, @@ -94,7 +96,7 @@ export class BasicContext implements IBasicContext { designer: IDesigner; registerInnerPlugins: () => Promise; innerSetters: InnerSetters; - innerSkeleton: InnerSkeleton; + innerSkeleton: ISkeleton; innerHotkey: IHotKey; innerPlugins: ILowCodePluginManager; canvas: IPublicApiCanvas; @@ -129,6 +131,7 @@ export class BasicContext implements IBasicContext { const skeleton = new Skeleton(innerSkeleton, 'any', true); const canvas = new Canvas(editor, true); const commonUI = new CommonUI(editor); + const innerCommand = new InnerCommand(); editor.set('setters', setters); editor.set('project', project); editor.set('material', material); @@ -162,6 +165,7 @@ export class BasicContext implements IBasicContext { context.setters = setters; context.material = material; const eventPrefix = meta?.eventPrefix || 'common'; + const commandScope = meta?.commandScope; context.event = new Event(commonEvent, { prefix: eventPrefix }); context.config = config; context.common = common; @@ -172,6 +176,9 @@ export class BasicContext implements IBasicContext { if (editorWindow) { context.editorWindow = new Window(editorWindow); } + context.command = new Command(innerCommand, context as IPublicModelPluginContext, { + commandScope, + }); context.registerLevel = registerLevel; context.isPluginRegisteredInWorkspace = registerLevel === IPublicEnumPluginRegisterLevel.Workspace; editor.set('pluginContext', context); diff --git a/packages/workspace/src/window.ts b/packages/workspace/src/window.ts index ce5aab4142..cd64a9b112 100644 --- a/packages/workspace/src/window.ts +++ b/packages/workspace/src/window.ts @@ -17,7 +17,7 @@ export interface IEditorWindow extends Omit, 'chan editorViews: Map; - editorView: IViewContext; + _editorView: IViewContext; changeViewName: (name: string, ignoreEmit?: boolean) => void; @@ -54,7 +54,7 @@ export class EditorWindow implements IEditorWindow { url: string | undefined; - @obx.ref editorView: Context; + @obx.ref _editorView: Context; @obx editorViews: Map = new Map(); @@ -62,6 +62,13 @@ export class EditorWindow implements IEditorWindow { sleep: boolean | undefined; + get editorView() { + if (!this._editorView) { + return this.editorViews.values().next().value; + } + return this._editorView; + } + constructor(readonly resource: IResource, readonly workspace: IWorkspace, private config: IWindowCOnfig) { makeObservable(this); this.title = config.title; @@ -75,10 +82,10 @@ export class EditorWindow implements IEditorWindow { updateState(state: WINDOW_STATE): void { switch (state) { case WINDOW_STATE.active: - this.editorView?.setActivate(true); + this._editorView?.setActivate(true); break; case WINDOW_STATE.inactive: - this.editorView?.setActivate(false); + this._editorView?.setActivate(false); break; case WINDOW_STATE.destroyed: break; @@ -146,7 +153,7 @@ export class EditorWindow implements IEditorWindow { for (let i = 0; i < editorViews.length; i++) { const name = editorViews[i].viewName; await this.initViewType(name); - if (!this.editorView) { + if (!this._editorView) { this.changeViewName(name); } } @@ -190,14 +197,14 @@ export class EditorWindow implements IEditorWindow { }; changeViewName = (name: string, ignoreEmit: boolean = true) => { - this.editorView?.setActivate(false); - this.editorView = this.editorViews.get(name)!; + this._editorView?.setActivate(false); + this._editorView = this.editorViews.get(name)!; - if (!this.editorView) { + if (!this._editorView) { return; } - this.editorView.setActivate(true); + this._editorView.setActivate(true); if (!ignoreEmit) { this.emitter.emit('window.change.view.type', name); diff --git a/scripts/build.sh b/scripts/build.sh index 828bb16d99..751e9094fe 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -10,6 +10,7 @@ lerna run build \ --scope @alilc/lowcode-editor-skeleton \ --scope @alilc/lowcode-designer \ --scope @alilc/lowcode-plugin-designer \ + --scope @alilc/lowcode-plugin-command \ --scope @alilc/lowcode-plugin-outline-pane \ --scope @alilc/lowcode-react-renderer \ --scope @alilc/lowcode-react-simulator-renderer \ diff --git a/scripts/sync.sh b/scripts/sync.sh index e9840eeca6..3edac03845 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -13,4 +13,5 @@ tnpm sync @alilc/lowcode-renderer-core tnpm sync @alilc/lowcode-react-renderer tnpm sync @alilc/lowcode-react-simulator-renderer tnpm sync @alilc/lowcode-engine -tnpm sync @alilc/lowcode-workspace \ No newline at end of file +tnpm sync @alilc/lowcode-workspace +tnpm sync @alilc/lowcode-plugin-command \ No newline at end of file